本文适用于在学习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);
函数接受一个函数并返回一个函数,该函数在创建函数时调用传递给该函数的函数。 抱歉,我无法停止写这篇文章。 现在以俄语显示:
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(), );
在这里有些人甚至可能以淫秽的形式提出一个问题:到底发生了什么? 为什么我们要创建一个函数本身不使用的变量? 如果外部函数很早就完成了执行,内部函数将如何访问它? 为什么两个创建的引用相同变量的函数得到不同的结果? 对所有这些问题的答案之一就是
关闭 。
每次
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取消此超时。 如果先前的超时已经完成,则什么都不会发生。 如果没有,那么对他来说更糟。
到此,也许我将结束。 我希望今天您学到了很多新东西,最重要的是,您了解了所学到的一切。 再见。