5. 作用域、闭包、高阶函数、静态绑定与动态绑定¶
5.1. 作用域、闭包、高阶函数¶
每个 变量 都是一个内存位置。每个变量都有一个名字,你可以在程序中用它来指代这个位置,还有一个与该位置相关联的值。
在语句 x = x + 1 中,最左边的 x 指的是位置还是值?提示: x 的值等于 x + 1 的值吗?最右边的“x”指的是位置还是值?提示:能把 1 加到一个位置上吗?
此外,每个变量还有一个 作用域 ,它由程序中该变量可见的所有部分构成。因此,当变量 x 在作用域内时,你可以用它的名字来指代它。在其他任何地方,使用名字 x 都是一个错误(除非存在另一个也叫 x 的变量也在作用域内,这一点下面会讨论)。
在 JavaScript 中,只有两种作用域:全局作用域和局部作用域。 局部作用域 总是由函数体定义的。这与 C、Java 这类语言不同,在那些语言中,作用域是由一对花括号包围的块定义的。例如,假设把下面的代码加载到 node 这样的解释器的读取-求值-打印循环中。
在大多数其他语言中(包括基于 ECMAScript 2016 及更高版本的 JavaScript),最好给每个变量尽可能小的作用域,办法是把变量声明在尽可能靠近其使用位置的地方。但在 ECMAScript 2016 之前的 JavaScript 中并非如此,它没有块级作用域,变量声明总是被 提升 到函数顶部。想一想这在下面这个例子中会如何表现,并记住把前面说过的内容考虑进去:在 JavaScript 中,作用域是由函数体定义的,而不是由内层的花括号定义的。
前面的例子说明了 JavaScript 中允许但被认为是不良做法的一种情况。在函数体内部,你绝不应该在任何嵌套块中声明变量。你应该始终把变量声明在最外层的块中(也就是定义函数体的那个块)。此外,始终把这些变量声明在该函数体的开头。
当一个表达式引用的变量在包含该表达式的最小作用域中没有声明时,该变量的使用是如何与某个声明绑定起来的?下面的代码说明了这种情况。
下面是另一个例子,说明在不同作用域层级使用同名变量的微妙之处。想一想把这段代码加载到读取-求值-打印解释器中会发生什么。
上面的例子表明,JavaScript(以及大多数其他函数式语言)使用 静态绑定 。也就是说,表达式 \(e\) 中对 x 的使用,绑定到在函数定义时包含 \(e\) 的最小作用域中出现的那个 x 声明。
静态绑定也称为 静态作用域 或 词法作用域 。
还有一种不同类型的绑定,称为 动态绑定 。在动态绑定中,对于表达式 \(e\) 中 x 的使用,首先在包含 \(e\) 的函数(比如 f )中查找与之绑定的声明,然后如果需要,再在调用 f 的函数(比如 g )中查找,接着在调用 g 的函数中查找,依此类推。注意,如果 JavaScript 使用动态绑定,那么前面例子中返回的值就会是 20 而不是 3。
我们看到,一个函数可以在另一个函数内部声明。此外,回忆一下,在函数式语言中(以及在我们所考虑的 JavaScript 子集中),函数是 一等值 。这意味着函数和任何其他值一样,可以作为函数调用的返回值。因此,一个函数可以返回它任意一个局部变量的值。这个被返回的变量绑定的是整数、布尔值还是函数,都没有区别。返回另一个函数(或以一个函数作为实参)的函数称为 高阶函数 ,如下面的例子所示。
var f = function () {
var add1 = function (x) { return x + 1; };
return add1;
}
var g = f(); // g is now the function that takes in one argument and adds 1 to it
g(5); // returns 6
f()(5); // same behavior as for the previous line
在这里,f 是一个高阶函数。然而,add1 不是高阶函数。那么 g 呢?
当一个函数 f 返回一个引用了 f 的某个参数或局部变量的局部函数时,会发生什么?如下例所示:
var f = function () {
var y = 1;
var addY = function (x) { return x + y; };
return addY;
}
var g = f(); // after f returns, the variable y
// in f is gone from the stack
g(5); // but g can still access it!
f()(5); // still returns 6
当一个局部函数引用定义在外层函数中的变量时,这个局部函数被实现为一个 闭包 ,也就是说,该局部函数不仅包含它自己的代码,还包含它的代码所引用的、在创建该局部函数时定义于其环境中的那些变量。
到目前为止,我们已经看到了三个 彼此不同且相互独立 的概念,它们都是函数式编程的核心:
作为一等值的函数与高阶函数,
闭包,以及
静态绑定。
由于 JavaScript 实现了上述所有特性,因此在 JavaScript 中使用函数式编程范式既自然又强大。它的一个强大之处在于,它允许我们像下面这个例子那样,在运行时轻松地构建新函数。
一旦你对函数作为一等值这一概念感到适应,你就会发现,上面例子中最后一张幻灯片所展示的 匿名函数 用法,是一种常常能提高代码可读性的技巧。为什么要用一个像 incr 这样的“临时变量”来存储一个本来完全可以直接返回的东西呢?
本节的第一道题是关于高阶函数的,用到了闭包和匿名函数。这道题是随机化的。你必须连续三次答对。
5.2. 高阶函数与匿名函数练习¶
这道题是关于高阶函数和作用域规则的。它用到了闭包和匿名函数。
5.3. 更多高阶函数与匿名函数练习¶
这道题使用与上一题相同的代码,并说明相同的主题。
5.4. 静态绑定与动态绑定规则练习¶
这道题使用与前两题相同的代码,但重点在于静态绑定规则与动态绑定规则之间的区别。

