第 13 条:使用立即调用的函数表达式创建局部作用域
这段程序(Bug 程序)输出什么?
function wrapElements(a) { var result = [], i, n; for (i = 0, n = a.length; i < n; i++) { result[i] = function() { return a[i]; }; } return result; } var wrapped = wrapElements([10, 20, 30, 40, 50]); var f = wrapped[0]; f(); // ?
程序员可能希望这段程序输出 10,但实际上它输出 undefined 值。
搞清楚该例子的方法是理解绑定与赋值的区别。在运行时进入一个作用域,JavaScript
会为每一个绑定到该作用域的变量在内存中分配一个“槽”(slot)。wrapElements 函数绑定了
三个局部变量:result、i 和 n。因此,当它被调用时,wrapElements 函数会为这三个变量分
配“槽”。在循环的每次迭代中,循环体都会为嵌套函数分配一个闭包。该程序的 Bug 在于
这样一个事实:程序员似乎期望该函数存储的是嵌套函数创建时变量 i 的值。但事实上,它
存储的是变量 i 的引用。由于每次函数创建后变量 i 的值都发生了变化,因此内部函数最终
看到的是变量 i 最后的值。值得注意的是,闭包存储的是其外部变量的引用而不是值。
所以,所有由 wrapElements 函数创建的闭包都引用在循环之前创建的变量 i 的同一个共
享“槽”。由于每次循环迭代都递增变量 i 直到运行到数组结束,因此,这时候其实当我们调
用其中任何一个闭包时,它都会查找数组的索引 5 并返回 undefined 值。
请注意,即使我们把 var 声明置于 for 循环的头部,wrapElements 函数的表现也完全
一样。
function wrapElements(a) { var result = []; for (var i = 0, n = a.length; i < n; i++) { result[i] = function() { return a[i]; }; } return result; } var wrapped = wrapElements([10, 20, 30, 40, 50]); var f = wrapped[0]; f(); // undefined
这个版本看起来更具欺骗性,因为 var 声明出现在了循环体中。但一如既往,变量声明
会被提升到循环的上方。再一次,变量 i 只被分配了一个“槽”。
解决的办法是通过创建一个嵌套函数并立即调用它来强制创建一个局部作用域。
function wrapElements(a) { var result = []; for (var i = 0, n = a.length; i < n; i++) { (function() { var j = i; result[i] = function() { return a[j]; }; })(); } return result; }
这种技术被称为立即调用的函数表达式,或 IIFE(发音为“ iffy”)。它是一种不可或缺
的解决 JavaScript 缺少块级作用域的方法。另一种变种是将作为形参的局部变量绑定到 IIFE
并将其值作为实参传入。
function wrapElements(a) { var result = []; for (var i = 0, n = a.length; i < n; i++) { (function(j) { result[i] = function() { return a[j]; }; })(i); } return result; }
然而,使用 IIFE 来创建局部作用域要小心,因为在函数中包裹代码块可能会导致代码
块发生一些微妙的变化。首先,代码块不能包含任何跳出块的 break 语句和 continue 语句。
因为在函数外使用 break 或 continue 是不合法的。其次,如果代码块引用了 this 或特别的
arguments 变量,IIFE 将会改变它们的含义。第 3 章将讨论与 this 和 arguments 变量一起工作
的技术。
提示
理解绑定与赋值的区别。 ❏
闭包通过引用而不是值捕获它们的外部变量。 ❏
使用立即调用的函数表达式(IIFE)来创建局部作用域。 ❏
当心在立即调用的函数表达式中包裹代码块可能改变其行为的情形。
来源:effective javascript书籍