JavaScript 权威指南第七版(GPT 重译)(三)(3)https://developer.aliyun.com/article/1485310
示例 8-1。将函数用作数据
// We define some simple functions here function add(x,y) { return x + y; } function subtract(x,y) { return x - y; } function multiply(x,y) { return x * y; } function divide(x,y) { return x / y; } // Here's a function that takes one of the preceding functions // as an argument and invokes it on two operands function operate(operator, operand1, operand2) { return operator(operand1, operand2); } // We could invoke this function like this to compute the value (2+3) + (4*5): let i = operate(add, operate(add, 2, 3), operate(multiply, 4, 5)); // For the sake of the example, we implement the simple functions again, // this time within an object literal; const operators = { add: (x,y) => x+y, subtract: (x,y) => x-y, multiply: (x,y) => x*y, divide: (x,y) => x/y, pow: Math.pow // This works for predefined functions too }; // This function takes the name of an operator, looks up that operator // in the object, and then invokes it on the supplied operands. Note // the syntax used to invoke the operator function. function operate2(operation, operand1, operand2) { if (typeof operators[operation] === "function") { return operatorsoperation; } else throw "unknown operator"; } operate2("add", "hello", operate2("add", " ", "world")) // => "hello world" operate2("pow", 10, 2) // => 100
8.4.1 定义自己的函数属性
在 JavaScript 中,函数不是原始值,而是一种特殊的对象,这意味着函数可以有属性。当一个函数需要一个“静态”变量,其值在调用之间保持不变时,通常方便使用函数本身的属性。例如,假设你想编写一个函数,每次调用时都返回一个唯一的整数。该函数可能两次返回相同的值。为了管理这个问题,函数需要跟踪它已经返回的值,并且这个信息必须在函数调用之间保持不变。你可以将这个信息存储在一个全局变量中,但这是不必要的,因为这个信息只被函数本身使用。最好将信息存储在 Function 对象的属性中。下面是一个示例,每次调用时都返回一个唯一的整数:
// Initialize the counter property of the function object. // Function declarations are hoisted so we really can // do this assignment before the function declaration. uniqueInteger.counter = 0; // This function returns a different integer each time it is called. // It uses a property of itself to remember the next value to be returned. function uniqueInteger() { return uniqueInteger.counter++; // Return and increment counter property } uniqueInteger() // => 0 uniqueInteger() // => 1
举个例子,考虑下面的factorial()
函数,它利用自身的属性(将自身视为数组)来缓存先前计算的结果:
// Compute factorials and cache results as properties of the function itself. function factorial(n) { if (Number.isInteger(n) && n > 0) { // Positive integers only if (!(n in factorial)) { // If no cached result factorial[n] = n * factorial(n-1); // Compute and cache it } return factorial[n]; // Return the cached result } else { return NaN; // If input was bad } } factorial[1] = 1; // Initialize the cache to hold this base case. factorial(6) // => 720 factorial[5] // => 120; the call above caches this value
8.5 函数作为命名空间
在函数内声明的变量在函数外部是不可见的。因此,有时候定义一个函数仅仅作为一个临时的命名空间是很有用的,你可以在其中定义变量而不会使全局命名空间混乱。
例如,假设你有一段 JavaScript 代码块,你想在许多不同的 JavaScript 程序中使用(或者对于客户端 JavaScript,在许多不同的网页上使用)。假设这段代码,像大多数代码一样,定义变量来存储计算的中间结果。问题在于,由于这段代码将在许多不同的程序中使用,你不知道它创建的变量是否会与使用它的程序创建的变量发生冲突。解决方案是将代码块放入一个函数中,然后调用该函数。这样,原本将是全局的变量变为函数的局部变量:
function chunkNamespace() { // Chunk of code goes here // Any variables defined in the chunk are local to this function // instead of cluttering up the global namespace. } chunkNamespace(); // But don't forget to invoke the function!
这段代码只定义了一个全局变量:函数名chunkNamespace
。如果即使定义一个属性也太多了,你可以在单个表达式中定义并调用一个匿名函数:
(function() { // chunkNamespace() function rewritten as an unnamed expression. // Chunk of code goes here }()); // End the function literal and invoke it now.
定义和调用一个函数的单个表达式的技术经常被使用,已经成为惯用语,并被称为“立即调用函数表达式”。请注意前面代码示例中括号的使用。在function
之前的开括号是必需的,因为没有它,JavaScript 解释器会尝试将function
关键字解析为函数声明语句。有了括号,解释器正确地将其识别为函数定义表达式。前导括号还有助于人类读者识别何时定义一个函数以立即调用,而不是为以后使用而定义。
当我们在命名空间函数内部定义一个或多个函数,并使用该命名空间内的变量,然后将它们作为命名空间函数的返回值传递出去时,函数作为命名空间的用法变得非常有用。这样的函数被称为闭包,它们是下一节的主题。
8.6 闭包
像大多数现代编程语言一样,JavaScript 使用词法作用域。这意味着函数在定义时使用的变量作用域,而不是在调用时使用的变量作用域。为了实现词法作用域,JavaScript 函数对象的内部状态必须包括函数的代码以及函数定义所在的作用域的引用。在计算机科学文献中,函数对象和作用域(一组变量绑定)的组合,用于解析函数变量的作用域,被称为闭包。
从技术上讲,所有的 JavaScript 函数都是闭包,但由于大多数函数是从定义它们的同一作用域中调用的,通常并不重要闭包是否涉及其中。当闭包从与其定义所在不同的作用域中调用时,闭包就变得有趣起来。这种情况最常见于从定义它的函数中返回嵌套函数对象时。有许多强大的编程技术涉及到这种嵌套函数闭包,它们在 JavaScript 编程中的使用变得相对常见。当你第一次遇到闭包时,它们可能看起来令人困惑,但重要的是你要足够了解它们以便舒适地使用它们。
理解闭包的第一步是复习嵌套函数的词法作用域规则。考虑以下代码:
let scope = "global scope"; // A global variable function checkscope() { let scope = "local scope"; // A local variable function f() { return scope; } // Return the value in scope here return f(); } checkscope() // => "local scope"
checkscope()
函数声明了一个局部变量,然后定义并调用一个返回该变量值的函数。你应该清楚为什么调用checkscope()
会返回“local scope”。现在,让我们稍微改变一下代码。你能告诉这段代码会返回什么吗?
let scope = "global scope"; // A global variable function checkscope() { let scope = "local scope"; // A local variable function f() { return scope; } // Return the value in scope here return f; } let s = checkscope()(); // What does this return?
在这段代码中,一对括号已经从checkscope()
内部移到了外部。现在,checkscope()
不再调用嵌套函数并返回其结果,而是直接返回嵌套函数对象本身。当我们在定义它的函数之外调用该嵌套函数(在代码的最后一行中的第二对括号中)时会发生什么?
记住词法作用域的基本规则:JavaScript 函数是在定义它们的作用域中执行的。嵌套函数f()
是在一个作用域中定义的,该作用域中变量scope
绑定到值“local scope”。当执行f
时,这个绑定仍然有效,无论从哪里执行。因此,前面代码示例的最后一行返回“local scope”,而不是“global scope”。这就是闭包的令人惊讶和强大的本质:它们捕获了它们所定义的外部函数的局部变量(和参数)绑定。
在§8.4.1 中,我们定义了一个uniqueInteger()
函数,该函数使用函数本身的属性来跟踪下一个要返回的值。这种方法的一个缺点是,有错误或恶意代码可能会重置计数器或将其设置为非整数,导致uniqueInteger()
函数违反其“unique”或“integer”部分的约定。闭包捕获了单个函数调用的局部变量,并可以将这些变量用作私有状态。下面是我们如何使用立即调用函数表达式来重新编写uniqueInteger()
,以定义一个命名空间和使用该命名空间来保持其状态私有的闭包:
let uniqueInteger = (function() { // Define and invoke let counter = 0; // Private state of function below return function() { return counter++; }; }()); uniqueInteger() // => 0 uniqueInteger() // => 1
要理解这段代码,你必须仔细阅读它。乍一看,代码的第一行看起来像是将一个函数赋给变量uniqueInteger
。实际上,代码正在定义并调用一个函数(第一行的开括号提示了这一点),因此将函数的返回值赋给了uniqueInteger
。现在,如果我们研究函数体,我们会发现它的返回值是另一个函数。正是这个嵌套函数对象被赋给了uniqueInteger
。嵌套函数可以访问其作用域中的变量,并且可以使用外部函数中定义的counter
变量。一旦外部函数返回,其他代码就无法看到counter
变量:内部函数对其具有独占访问权限。
像counter
这样的私有变量不一定是单个闭包的专有:完全可以在同一个外部函数中定义两个或更多个嵌套函数并共享相同的作用域。考虑以下代码:
function counter() { let n = 0; return { count: function() { return n++; }, reset: function() { n = 0; } }; } let c = counter(), d = counter(); // Create two counters c.count() // => 0 d.count() // => 0: they count independently c.reset(); // reset() and count() methods share state c.count() // => 0: because we reset c d.count() // => 1: d was not reset
counter()
函数返回一个“计数器”对象。这个对象有两个方法:count()
返回下一个整数,reset()
重置内部状态。首先要理解的是,这两个方法共享对私有变量n
的访问。其次要理解的是,每次调用counter()
都会创建一个新的作用域——独立于先前调用使用的作用域,并在该作用域内创建一个新的私有变量。因此,如果您两次调用counter()
,您将得到两个具有不同私有变量的计数器对象。在一个计数器对象上调用count()
或reset()
对另一个没有影响。
值得注意的是,您可以将闭包技术与属性的 getter 和 setter 结合使用。下面这个counter()
函数的版本是§6.10.6 中出现的代码的变体,但它使用闭包来实现私有状态,而不是依赖于常规对象属性:
function counter(n) { // Function argument n is the private variable return { // Property getter method returns and increments private counter var. get count() { return n++; }, // Property setter doesn't allow the value of n to decrease set count(m) { if (m > n) n = m; else throw Error("count can only be set to a larger value"); } }; } let c = counter(1000); c.count // => 1000 c.count // => 1001 c.count = 2000; c.count // => 2000 c.count = 2000; // !Error: count can only be set to a larger value
注意,这个counter()
函数的版本并没有声明一个局部变量,而是只是使用其参数n
来保存属性访问方法共享的私有状态。这允许counter()
的调用者指定私有变量的初始值。
示例 8-2 是通过我们一直在演示的闭包技术对共享私有状态进行泛化的一个例子。这个示例定义了一个addPrivateProperty()
函数,该函数定义了一个私有变量和两个嵌套函数来获取和设置该变量的值。它将这些嵌套函数作为您指定对象的方法添加。
示例 8-2. 使用闭包的私有属性访问方法
// This function adds property accessor methods for a property with // the specified name to the object o. The methods are named get<name> // and set<name>. If a predicate function is supplied, the setter // method uses it to test its argument for validity before storing it. // If the predicate returns false, the setter method throws an exception. // // The unusual thing about this function is that the property value // that is manipulated by the getter and setter methods is not stored in // the object o. Instead, the value is stored only in a local variable // in this function. The getter and setter methods are also defined // locally to this function and therefore have access to this local variable. // This means that the value is private to the two accessor methods, and it // cannot be set or modified except through the setter method. function addPrivateProperty(o, name, predicate) { let value; // This is the property value // The getter method simply returns the value. o[`get${name}`] = function() { return value; }; // The setter method stores the value or throws an exception if // the predicate rejects the value. o[`set${name}`] = function(v) { if (predicate && !predicate(v)) { throw new TypeError(`set${name}: invalid value ${v}`); } else { value = v; } }; } // The following code demonstrates the addPrivateProperty() method. let o = {}; // Here is an empty object // Add property accessor methods getName and setName() // Ensure that only string values are allowed addPrivateProperty(o, "Name", x => typeof x === "string"); o.setName("Frank"); // Set the property value o.getName() // => "Frank" o.setName(0); // !TypeError: try to set a value of the wrong type
现在我们已经看到了许多例子,其中两个闭包在同一个作用域中定义并共享对相同私有变量或变量的访问。这是一个重要的技术,但同样重要的是要认识到闭包无意中共享对不应共享的变量的访问。考虑以下代码:
// This function returns a function that always returns v function constfunc(v) { return () => v; } // Create an array of constant functions: let funcs = []; for(var i = 0; i < 10; i++) funcs[i] = constfunc(i); // The function at array element 5 returns the value 5. funcs[5]() // => 5
在处理像这样使用循环创建多个闭包的代码时,一个常见的错误是尝试将循环移到定义闭包的函数内部。例如,考虑以下代码:
// Return an array of functions that return the values 0-9 function constfuncs() { let funcs = []; for(var i = 0; i < 10; i++) { funcs[i] = () => i; } return funcs; } let funcs = constfuncs(); funcs[5]() // => 10; Why doesn't this return 5?
这段代码创建了 10 个闭包并将它们存储在一个数组中。这些闭包都在同一个函数调用中定义,因此它们共享对变量i
的访问。当constfuncs()
返回时,变量i
的值为 10,所有 10 个闭包都共享这个值。因此,返回的函数数组中的所有函数都返回相同的值,这并不是我们想要的。重要的是要记住,与闭包相关联的作用域是“活动的”。嵌套函数不会创建作用域的私有副本,也不会对变量绑定进行静态快照。从根本上说,这里的问题是使用var
声明的变量在整个函数中都被定义。我们的for
循环使用var i
声明循环变量,因此变量i
在整个函数中被定义,而不是更窄地限制在循环体内。这段代码展示了 ES5 及之前版本中常见的一类错误,但 ES6 引入的块作用域变量解决了这个问题。如果我们只是用let
或const
替换var
,问题就消失了。因为let
和const
是块作用域的,循环的每次迭代都定义了一个独立于所有其他迭代的作用域,并且每个作用域都有自己独立的i
绑定。
写闭包时要记住的另一件事是,this
是 JavaScript 关键字,而不是变量。正如前面讨论的,箭头函数继承了包含它们的函数的this
值,但使用function
关键字定义的函数不会。因此,如果您编写一个需要使用其包含函数的this
值的闭包,您应该在返回之前使用箭头函数或调用bind()
,或将外部this
值分配给闭包将继承的变量:
const self = this; // Make the this value available to nested functions
8.7 函数属性、方法和构造函数
我们已经看到函数在 JavaScript 程序中是值。当应用于函数时,typeof
运算符返回字符串“function”,但函数实际上是 JavaScript 对象的一种特殊类型。由于函数是对象,它们可以像任何其他对象一样具有属性和方法。甚至有一个Function()
构造函数来创建新的函数对象。接下来的小节记录了length
、name
和prototype
属性;call()
、apply()
、bind()
和toString()
方法;以及Function()
构造函数。
8.7.1 length 属性
函数的只读length
属性指定函数的arity——它在参数列表中声明的参数数量,通常是函数期望的参数数量。如果函数有一个剩余参数,那么这个参数不会计入length
属性的目的。
8.7.2 名称属性
函数的只读name
属性指定函数在定义时使用的名称,如果它是用名称定义的,或者在创建时未命名的函数表达式被分配给的变量或属性的名称。当编写调试或错误消息时,此属性非常有用。
8.7.3 prototype 属性
所有函数,除了箭头函数,都有一个prototype
属性,指向一个称为原型对象的对象。每个函数都有一个不同的原型对象。当一个函数被用作构造函数时,新创建的对象会从原型对象继承属性。原型和prototype
属性在§6.2.3 中讨论过,并将在第九章中再次涉及。
8.7.4 call()和 apply()方法
call()
和apply()
允许您间接调用(§8.2.4)一个函数,就好像它是另一个对象的方法一样。call()
和apply()
的第一个参数是要调用函数的对象;这个参数是调用上下文,并在函数体内成为this
关键字的值。要将函数f()
作为对象o
的方法调用(不传递参数),可以使用call()
或apply()
:
f.call(o); f.apply(o);
这两行代码中的任何一行与以下代码类似(假设o
尚未具有名为m
的属性):
o.m = f; // Make f a temporary method of o. o.m(); // Invoke it, passing no arguments. delete o.m; // Remove the temporary method.
请记住,箭头函数继承了定义它们的上下文的this
值。这不能通过call()
和apply()
方法覆盖。如果在箭头函数上调用这些方法之一,第一个参数实际上会被忽略。
在第一个调用上下文参数之后的任何call()
参数都是传递给被调用函数的值(对于箭头函数,这些参数不会被忽略)。例如,要向函数f()
传递两个数字,并将其作为对象o
的方法调用,可以使用以下代码:
f.call(o, 1, 2);
apply()
方法类似于call()
方法,只是要传递给函数的参数被指定为一个数组:
f.apply(o, [1,2]);
如果一个函数被定义为接受任意数量的参数,apply()
方法允许你在任意长度的数组内容上调用该函数。在 ES6 及更高版本中,我们可以直接使用扩展运算符,但你可能会看到使用 apply()
而不是扩展运算符的 ES5 代码。例如,要在不使用扩展运算符的情况下找到数组中的最大数,你可以使用 apply()
方法将数组的元素传递给 Math.max()
函数:
let biggest = Math.max.apply(Math, arrayOfNumbers);
下面定义的 trace()
函数类似于 §8.3.4 中定义的 timed()
函数,但它适用于方法而不是函数。它使用 apply()
方法而不是扩展运算符,通过这样做,它能够以与包装方法相同的参数和 this
值调用被包装的方法:
// Replace the method named m of the object o with a version that logs // messages before and after invoking the original method. function trace(o, m) { let original = o[m]; // Remember original method in the closure. o[m] = function(...args) { // Now define the new method. console.log(new Date(), "Entering:", m); // Log message. let result = original.apply(this, args); // Invoke original. console.log(new Date(), "Exiting:", m); // Log message. return result; // Return result. }; }
8.7.5 bind()
方法
bind()
的主要目的是将函数绑定到对象。当你在函数 f
上调用 bind()
方法并传递一个对象 o
时,该方法会返回一个新函数。调用新函数(作为函数)会将原始函数 f
作为 o
的方法调用。传递给新函数的任何参数都会传递给原始函数。例如:
function f(y) { return this.x + y; } // This function needs to be bound let o = { x: 1 }; // An object we'll bind to let g = f.bind(o); // Calling g(x) invokes f() on o g(2) // => 3 let p = { x: 10, g }; // Invoke g() as a method of this object p.g(2) // => 3: g is still bound to o, not p.
箭头函数从定义它们的环境继承它们的 this
值,并且该值不能被 bind()
覆盖,因此如果前面代码中的函数 f()
被定义为箭头函数,绑定将不起作用。然而,调用 bind()
最常见的用例是使非箭头函数的行为类似箭头函数,因此在实践中,对绑定箭头函数的限制并不是问题。
bind()
方法不仅仅是将函数绑定到对象,它还可以执行部分应用:在第一个参数之后传递给 bind()
的任何参数都与 this
值一起绑定。bind()
的这种部分应用特性适用于箭头函数。部分应用是函数式编程中的常见技术,有时被称为柯里化。以下是 bind()
方法用于部分应用的一些示例:
let sum = (x,y) => x + y; // Return the sum of 2 args let succ = sum.bind(null, 1); // Bind the first argument to 1 succ(2) // => 3: x is bound to 1, and we pass 2 for the y argument function f(y,z) { return this.x + y + z; } let g = f.bind({x: 1}, 2); // Bind this and y g(3) // => 6: this.x is bound to 1, y is bound to 2 and z is 3
由 bind()
返回的函数的 name
属性是调用 bind()
的函数的名称属性,前缀为“bound”。
8.7.6 toString()
方法
像所有 JavaScript 对象一样,函数有一个 toString()
方法。ECMAScript 规范要求该方法返回一个遵循函数声明语法的字符串。实际上,大多数(但不是所有)实现这个 toString()
方法的实现会返回函数的完整源代码。内置函数通常返回一个包含类似“[native code]”的字符串作为函数体的字符串。
8.7.7 Function()
构造函数
因为函数是对象,所以有一个 Function()
构造函数可用于创建新函数:
const f = new Function("x", "y", "return x*y;");
这行代码创建了一个新函数,它与使用熟悉语法定义的函数更或多少等效:
const f = function(x, y) { return x*y; };
Function()
构造函数期望任意数量的字符串参数。最后一个参数是函数体的文本;它可以包含任意 JavaScript 语句,用分号分隔。构造函数的所有其他参数都是指定函数参数名称的字符串。如果你定义一个不带参数的函数,你只需将一个字符串(函数体)传递给构造函数。
注意 Function()
构造函数没有传递任何指定创建的函数名称的参数。与函数字面量一样,Function()
构造函数创建匿名函数。
有几点很重要需要了解关于 Function()
构造函数:
Function()
构造函数允许在运行时动态创建和编译 JavaScript 函数。Function()
构造函数解析函数体并在每次调用时创建一个新的函数对象。如果构造函数的调用出现在循环中或在频繁调用的函数内部,这个过程可能效率低下。相比之下,在循环中出现的嵌套函数和函数表达式在遇到时不会重新编译。- 关于
Function()
构造函数的最后一个非常重要的观点是,它创建的函数不使用词法作用域;相反,它们总是被编译为顶级函数,如下面的代码所示:
let scope = "global"; function constructFunction() { let scope = "local"; return new Function("return scope"); // Doesn't capture local scope! } // This line returns "global" because the function returned by the // Function() constructor does not use the local scope. constructFunction()() // => "global"
Function()
构造函数最好被视为eval()
的全局作用域版本(参见§4.12.2),它在自己的私有作用域中定义新的变量和函数。你可能永远不需要在你的代码中使用这个构造函数。
8.8 函数式编程
JavaScript 不像 Lisp 或 Haskell 那样是一种函数式编程语言,但 JavaScript 可以将函数作为对象进行操作的事实意味着我们可以在 JavaScript 中使用函数式编程技术。数组方法如map()
和reduce()
特别适合函数式编程风格。接下来的部分演示了 JavaScript 中函数式编程的技术。它们旨在探索 JavaScript 函数的强大功能,而不是规范良好的编程风格。
8.8.1 使用函数处理数组
假设我们有一个数字数组,我们想要计算这些值的均值和标准差。我们可以像这样以非函数式的方式进行:
let data = [1,1,3,5,5]; // This is our array of numbers // The mean is the sum of the elements divided by the number of elements let total = 0; for(let i = 0; i < data.length; i++) total += data[i]; let mean = total/data.length; // mean == 3; The mean of our data is 3 // To compute the standard deviation, we first sum the squares of // the deviation of each element from the mean. total = 0; for(let i = 0; i < data.length; i++) { let deviation = data[i] - mean; total += deviation * deviation; } let stddev = Math.sqrt(total/(data.length-1)); // stddev == 2
我们可以使用数组方法map()
和reduce()
以简洁的函数式风格执行相同的计算,如下所示(参见§7.8.1 回顾这些方法):
// First, define two simple functions const sum = (x,y) => x+y; const square = x => x*x; // Then use those functions with Array methods to compute mean and stddev let data = [1,1,3,5,5]; let mean = data.reduce(sum)/data.length; // mean == 3 let deviations = data.map(x => x-mean); let stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1)); stddev // => 2
这个新版本的代码看起来与第一个版本非常不同,但仍然在对象上调用方法,因此仍然保留了一些面向对象的约定。让我们编写map()
和reduce()
方法的函数式版本:
const map = function(a, ...args) { return a.map(...args); }; const reduce = function(a, ...args) { return a.reduce(...args); };
有了这些定义的map()
和reduce()
函数,我们现在计算均值和标准差的代码如下:
const sum = (x,y) => x+y; const square = x => x*x; let data = [1,1,3,5,5]; let mean = reduce(data, sum)/data.length; let deviations = map(data, x => x-mean); let stddev = Math.sqrt(reduce(map(deviations, square), sum)/(data.length-1)); stddev // => 2
8.8.2 高阶函数
高阶函数是一个操作函数的函数,它接受一个或多个函数作为参数并返回一个新函数。这里有一个例子:
// This higher-order function returns a new function that passes its // arguments to f and returns the logical negation of f's return value; function not(f) { return function(...args) { // Return a new function let result = f.apply(this, args); // that calls f return !result; // and negates its result. }; } const even = x => x % 2 === 0; // A function to determine if a number is even const odd = not(even); // A new function that does the opposite [1,1,3,5,5].every(odd) // => true: every element of the array is odd
这个not()
函数是一个高阶函数,因为它接受一个函数参数并返回一个新函数。再举一个例子,考虑接下来的mapper()
函数。它接受一个函数参数并返回一个使用该函数将一个数组映射到另一个数组的新函数。这个函数使用了之前定义的map()
函数,你需要理解这两个函数的不同之处很重要:
// Return a function that expects an array argument and applies f to // each element, returning the array of return values. // Contrast this with the map() function from earlier. function mapper(f) { return a => map(a, f); } const increment = x => x+1; const incrementAll = mapper(increment); incrementAll([1,2,3]) // => [2,3,4]
这里是另一个更一般的例子,它接受两个函数f
和g
,并返回一个计算f(g())
的新函数:
// Return a new function that computes f(g(...)). // The returned function h passes all of its arguments to g, then passes // the return value of g to f, then returns the return value of f. // Both f and g are invoked with the same this value as h was invoked with. function compose(f, g) { return function(...args) { // We use call for f because we're passing a single value and // apply for g because we're passing an array of values. return f.call(this, g.apply(this, args)); }; } const sum = (x,y) => x+y; const square = x => x*x; compose(square, sum)(2,3) // => 25; the square of the sum
在接下来的部分中定义的partial()
和memoize()
函数是另外两个重要的高阶函数。
8.8.3 函数的部分应用
函数f
的bind()
方法(参见§8.7.5)返回一个在指定上下文中调用f
并带有指定参数集的新函数。我们说它将函数绑定到一个对象并部分应用参数。bind()
方法在左侧部分应用参数,也就是说,你传递给bind()
的参数被放在传递给原始函数的参数列表的开头。但也可以在右侧部分应用参数:
// The arguments to this function are passed on the left function partialLeft(f, ...outerArgs) { return function(...innerArgs) { // Return this function let args = [...outerArgs, ...innerArgs]; // Build the argument list return f.apply(this, args); // Then invoke f with it }; } // The arguments to this function are passed on the right function partialRight(f, ...outerArgs) { return function(...innerArgs) { // Return this function let args = [...innerArgs, ...outerArgs]; // Build the argument list return f.apply(this, args); // Then invoke f with it }; } // The arguments to this function serve as a template. Undefined values // in the argument list are filled in with values from the inner set. function partial(f, ...outerArgs) { return function(...innerArgs) { let args = [...outerArgs]; // local copy of outer args template let innerIndex=0; // which inner arg is next // Loop through the args, filling in undefined values from inner args for(let i = 0; i < args.length; i++) { if (args[i] === undefined) args[i] = innerArgs[innerIndex++]; } // Now append any remaining inner arguments args.push(...innerArgs.slice(innerIndex)); return f.apply(this, args); }; } // Here is a function with three arguments const f = function(x,y,z) { return x * (y - z); }; // Notice how these three partial applications differ partialLeft(f, 2)(3,4) // => -2: Bind first argument: 2 * (3 - 4) partialRight(f, 2)(3,4) // => 6: Bind last argument: 3 * (4 - 2) partial(f, undefined, 2)(3,4) // => -6: Bind middle argument: 3 * (2 - 4)
这些部分应用函数使我们能够轻松地从已定义的函数中定义有趣的函数。以下是一些示例:
const increment = partialLeft(sum, 1); const cuberoot = partialRight(Math.pow, 1/3); cuberoot(increment(26)) // => 3
当我们将部分应用与其他高阶函数结合时,部分应用变得更加有趣。例如,以下是使用组合和部分应用定义前面刚刚展示的not()
函数的一种方法:
const not = partialLeft(compose, x => !x); const even = x => x % 2 === 0; const odd = not(even); const isNumber = not(isNaN); odd(3) && isNumber(2) // => true
我们还可以使用组合和部分应用来以极端函数式风格重新执行我们的均值和标准差计算:
// sum() and square() functions are defined above. Here are some more: const product = (x,y) => x*y; const neg = partial(product, -1); const sqrt = partial(Math.pow, undefined, .5); const reciprocal = partial(Math.pow, undefined, neg(1)); // Now compute the mean and standard deviation. let data = [1,1,3,5,5]; // Our data let mean = product(reduce(data, sum), reciprocal(data.length)); let stddev = sqrt(product(reduce(map(data, compose(square, partial(sum, neg(mean)))), sum), reciprocal(sum(data.length,neg(1))))); [mean, stddev] // => [3, 2]
请注意,这段用于计算均值和标准差的代码完全是函数调用;没有涉及运算符,并且括号的数量已经变得如此之多,以至于这段 JavaScript 代码开始看起来像 Lisp 代码。再次强调,这不是我推崇的 JavaScript 编程风格,但看到 JavaScript 代码可以有多函数式是一个有趣的练习。
8.8.4 Memoization
在§8.4.1 中,我们定义了一个阶乘函数,它缓存了先前计算的结果。在函数式编程中,这种缓存称为memoization。接下来的代码展示了一个高阶函数,memoize()
,它接受一个函数作为参数,并返回该函数的一个记忆化版本:
// Return a memoized version of f. // It only works if arguments to f all have distinct string representations. function memoize(f) { const cache = new Map(); // Value cache stored in the closure. return function(...args) { // Create a string version of the arguments to use as a cache key. let key = args.length + args.join("+"); if (cache.has(key)) { return cache.get(key); } else { let result = f.apply(this, args); cache.set(key, result); return result; } }; }
memoize()
函数创建一个新对象用作缓存,并将此对象分配给一个局部变量,以便它对(在返回的函数的闭包中)是私有的。返回的函数将其参数数组转换为字符串,并将该字符串用作缓存对象的属性名。如果缓存中存在值,则直接返回它。否则,调用指定的函数来计算这些参数的值,缓存该值,并返回它。以下是我们如何使用memoize()
:
// Return the Greatest Common Divisor of two integers using the Euclidian // algorithm: http://en.wikipedia.org/wiki/Euclidean_algorithm function gcd(a,b) { // Type checking for a and b has been omitted if (a < b) { // Ensure that a >= b when we start [a, b] = [b, a]; // Destructuring assignment to swap variables } while(b !== 0) { // This is Euclid's algorithm for GCD [a, b] = [b, a%b]; } return a; } const gcdmemo = memoize(gcd); gcdmemo(85, 187) // => 17 // Note that when we write a recursive function that we will be memoizing, // we typically want to recurse to the memoized version, not the original. const factorial = memoize(function(n) { return (n <= 1) ? 1 : n * factorial(n-1); }); factorial(5) // => 120: also caches values for 4, 3, 2 and 1.
8.9 总结
关于本章的一些关键要点如下:
- 您可以使用
function
关键字和 ES6 的=>
箭头语法定义函数。 - 您可以调用函数,这些函数可以用作方法和构造函数。
- 一些 ES6 功能允许您为可选函数参数定义默认值,使用 rest 参数将多个参数收集到一个数组中,并将对象和数组参数解构为函数参数。
- 您可以使用
...
扩展运算符将数组或其他可迭代对象的元素作为参数传递给函数调用。 - 在封闭函数内部定义并返回的函数保留对其词法作用域的访问权限,因此可以读取和写入外部函数中定义的变量。以这种方式使用的函数称为closures,这是一种值得理解的技术。
- 函数是 JavaScript 可以操作的对象,这使得函数式编程成为可能。
¹ 这个术语是由 Martin Fowler 创造的。参见http://martinfowler.com/dslCatalog/methodChaining.html。
² 如果你熟悉 Python,注意这与 Python 不同,其中每次调用都共享相同的默认值。
³ 这可能看起来不是特别有趣,除非您熟悉更静态的语言,在这些语言中,函数是程序的一部分,但不能被程序操纵。