JS:年轻战士课程中的高阶函数

本文适用于在学习JavaScript的棘手道路上迈出怯first第一步的人。 尽管事实上在2018年,我还是使用ES5语法,以便正在HTML学院学习JavaScript Level 1课程的年轻Padawans可以理解本文。

将JS与许多其他编程语言区分开的功能之一是,在该语言中,函数是“一流的对象”。 或者,在俄语中,功能就是含义。 与数字,字符串或对象相同。 我们可以将函数写入变量,可以将其放入数组或对象属性中。 我们甚至可以添加两个功能。 实际上,这没有任何意义,但是事实上-我们可以!

function hello(){}; function world(){}; console.log(hello + world); //  ,  ,   //   ,     

最有趣的是,我们可以创建对其他函数进行操作的函数-接受它们作为参数或将它们返回为值。 这些函数称为高阶函数 。 今天,我们的男孩和女孩,将谈论如何使这一机会适应国民经济的需求。 在此过程中,您将了解有关JS函数的一些有用功能的更多信息。

流水线


假设我们有一件事情需要您做很多事情。 假设某个用户上传了一个文本文件,该文件以JSON格式存储数据,而我们想处理其内容。 首先,我们需要修剪多余的空白字符,这些空白字符可能会由于用户操作或操作系统而“增长”。 然后检查文本中是否没有恶意代码(谁知道这些用户)。 然后使用JSON.parse方法从文本转换为对象。 然后从该对象中删除我们需要的数据。 最后-将这些数据发送到服务器。 您得到的是这样的:

 function trim(){/*  */}; function sanitize(){/*  */}; function parse(){/*  */}; function extractData(){/*  */}; function send(){/*  */}; var textFromFile = getTextFromFile(); send(extractData(parse(sanitize(trim(testFromFile)))); 

看起来很同意。 此外,您可能没有注意到缺少一个结束括号。 当然,IDE会告诉您这一点,但是仍然存在问题。 为了解决这个问题,最近提出了一个新的运算符|> 。 实际上,它不是新事物,而是从功能语言中诚实地借用的,但这不是重点。 使用此运算符,可以将最后一行重写如下:

 textFromFile |> trim |> sanitize |> parse |> extractData |> send; 

|>运算符将其左操作数作为参数传递给右操作数。 例如, "Hello" |> console.log等效于console.log("Hello") 。 恰好在沿着链调用多个函数的情况下,这非常方便。 但是,在引入此运算符之前,将花费很多时间(如果完全接受此建议),但是您现在必须以某种方式生活。 因此,我们可以为自行车编写一个模拟此行为函数:

 function pipe(){ var args = Array.from(arguments); var result = args.shift(); while(args.length){ var f = args.shift(); result = f(result); } return result; } pipe( textFromFile, trim, sanitize, parse, extractData, send ); 

如果您是新手javascript专家(javascript?Javascript?),则函数的第一行对您来说似乎很难理解。 很简单:在函数内部,我们使用arguments关键字访问包含传递给函数的所有参数的类似数组的对象。 当我们事先不知道她会有多少论点时,这非常方便。 大型对象就像一个数组,但不尽相同。 因此,我们使用Array.from方法将其转换为普通数组。 我希望进一步的代码已经很容易理解:我们从左到右开始从数组中提取元素,并以与|>运算符相同的方式将它们彼此应用。

记录中


这是另一个接近现实生活的例子。 假设我们已经有一个函数f ,它确实有用。 在测试我们的代码的过程中,我们想了解更多有关f是如何执行的信息。 在什么时候被调用,传递给它什么参数,返回什么值。

当然,对于每个函数调用,我们都可以这样编写:

 var result = f(a, b); console.log(" f     " + a + "  " + b + "   " + result); console.log(" : " + Date.now()); 

但是,首先,这很麻烦。 其次,很容易忘记它。 有一天,我们将简单地写下f(a, b) ,从那时起,无知的黑暗就会沉入我们的脑海。 它将随着每个新的挑战f而扩展,我们对此一无所知。

理想情况下,我希望日志自动发生。 这样,每次调用f ,我们需要的所有内容都会写入控制台。 而且,幸运的是,我们有办法做到这一点。 满足新的更高阶功能!

 function addLogger(f){ return function(){ var args = Array.from(arguments); var result = f.apply(null, args); console.log(" " + f.name + "     " + args.join() + "    " + result + "\n" + " : " + Date.now()); return result; } } function sum(a, b){ return a + b; } var sumWithLogging = addLogger(sum); sum(1, 2); //   sumWithLogging(1, 2) //  

函数接受一个函数并返回一个函数,该函数在创建函数时调用传递给该函数的函数。 抱歉,我无法停止写这篇文章。 现在以俄语显示: addLogger函数围绕作为参数传递给它的函数创建一个addLogger 。 包装也是一种功能。 当被调用时,它以与前面的示例相同的方式收集其参数数组。 然后,使用apply方法,它将调用具有相同参数的包装函数并记住结果。 之后,包装器将所有内容写入控制台。

这里有经典的中间人攻击案例。 如果使用包装器而不是f ,那么从使用包装器的代码的角度来看,实际上没有什么区别。 该代码可以假定它直接与f通信。 同时,包装人员将所有情况报告给少校同志。

Eins,Zwei,Drei,Vier ...


还有一项接近实践的任务。 假设我们需要编号一些实体。 每次出现新实体时,我们都会为其获取一个新编号,该编号比前一个多。 为此,我们启动以下形式的函数:

 var lastNumber = 0; function getNewNumber(){ return lastNumber++; } 

然后我们有了一种新的实体。 说,在此之前,我们给兔子编号了,现在也有兔子了。 如果您同时使用一个函数和其他函数,则发给兔子的每个数字都会在发给兔子的一系列数字中产生“空洞”。 因此,我们需要第二个函数,并带有第二个变量:

 var lastHareNumber = 0; function getNewHareNumber(){ return lastHareNumber++; } var lastRabbitNumber = 0; function getNewRabbitNumber(){ return lastRabbitNumber++; } 

您觉得这段代码难闻吗? 我想要更好的东西。 首先,我希望能够在不重复代码的情况下声明此类函数。 其次,我想以某种方式将函数使用的变量“打包”到函数本身中,以免再次阻塞命名空间。

然后一个男人突然熟悉OOP的概念,说道:
“小学,沃森。” 有必要使数字生成器不是对象,而是对象。 对象只是设计用来存储与数据一起使用的功能以及这些数据。 然后,我们可以编写如下内容:

 var numberGenerator = new NumberGenerator(); var n = numberGenerator.get(); 

我将回答:
-老实说,我完全同意你的看法。 原则上,这是比我现在提供的方法更正确的方法。 但是这里我们有一篇关于函数的文章,而不是关于OOP的文章。 那么,您可以保持安静一段时间,让我结束吗?

在这里(惊奇!)高阶函数将再次为我们提供帮助。

 function createNumberGenerator(){ var n = 0; return function(){ return n++; } } var getNewHareNumber = createNumberGenerator(); var getNewRabbitNumber = createNumberGenerator(); console.log( getNewHareNumber(), getNewHareNumber(), getNewHareNumber(), getNewRabbitNumber(), getNewRabbitNumber(), ); //    0, 1, 2, 0, 1 

在这里有些人甚至可能以淫秽的形式提出一个问题:到底发生了什么? 为什么我们要创建一个函数本身不使用的变量? 如果外部函数很早就完成了执行,内部函数将如何访问它? 为什么两个创建的引用相同变量的函数得到不同的结果? 对所有这些问题的答案之一就是关闭

每次createNumberGenerator函数时,JS解释器都会创建一个神奇的东西,称为“执行上下文”。 粗略地说,这是一个对象,其中存储了在此函数中声明的所有变量。 我们不能作为普通的javascript对象来访问它,但是确实如此。

如果函数是“简单的”(例如,加上数字),那么在工作结束后,执行上下文将变得无用。 您知道JS中不必要的数据会发生什么情况吗? 他们被一个名为垃圾收集器的贪得无厌的恶魔吞噬。 但是,如果该功能“复杂”,则即使执行了该功能,也可能有人仍需要其上下文。 在这种情况下,垃圾收集器会饶恕他,并且他仍会挂在他的记忆中,以便那些需要他的人仍然可以使用他。

因此,由createNumberGenerator返回的createNumberGenerator将始终有权访问其自己的变量n副本。 您可以将其视为D&D的“手提袋”。 您将手放在包里,发现自己在一个个人的多维“口袋”中,可以存放所需的所有物品。

去抖


有一种“消除反弹”的东西。 这是我们不希望某些函数被频繁调用的时候。 假设有一个按钮,单击该按钮将启动“昂贵”(耗时长,或占用大量内存,或Internet,或牺牲处女)过程。 不耐烦的用户可能开始以超过十赫兹的频率单击此按钮。 此外,上述过程具有这样的性质:连续运行十次没有任何意义,因为最终结果不会改变。 然后就是我们应用“消除抖动”。

它的本质非常简单:我们不是立即执行功能,而是在一段时间后执行。 如果在此时间之前,函数再次被调用,我们将“重置计时器”。 因此,用户可以单击按钮至少一千次-牺牲只需要一次。 但是,更少的单词,更多的代码:

 function debounce(f, delay){ var lastTimeout; return function(){ if(lastTimeout){ clearTimeout(lastTimeout); } var args = Array.from(arguments); lastTimeout = setTimeout(function(){ f.apply(null, args); }, delay); } } function sacrifice(name){ console.log(name + "     * *"); } function sacrificeDebounced = debounce(sacrifice, 500); sacrificeDebounced(""); sacrificeDebounced(""); sacrificeDebounced(""); 

Lena将在半秒内牺牲,而Katya和Sveta将得益于我们的神奇功能而幸存下来。

如果您仔细阅读了前面的示例,则应该对这里的所有工作方式有很好的了解。 通过debounce创建的包装函数使用setTimeout触发原始函数的延迟执行。 在这种情况下,超时标识符存储在lastTimeout变量中,由于关闭,包装程序可以访问该变量。 如果超时标识符已经在此变量中,则包装器使用clearTimeout取消此超时。 如果先前的超时已经完成,则什么都不会发生。 如果没有,那么对他来说更糟。

到此,也许我将结束。 我希望今天您学到了很多新东西,最重要的是,您了解了所学到的一切。 再见。

Source: https://habr.com/ru/post/zh-CN421537/


All Articles