正因为这个特性,很多人都会犯一个非常常见的错误: 当在循环中创建了函数,然后将循环的索引值和每个函数绑定的时候,通常得到的结果不是预期的(预期是希望每个函数都能够获取各自对应的索引值)。这个错误也是题目中说提到的那段代码的最大错误的地方,下面我们来揭秘为啥button点击弹出来的都是5.
var data = []; for (var k = 0; k < 3; k++) { data[k] = function () { alert(k); }; } data[0](); // 3, 而不是 0 data[1](); // 3, 而不是 1 data[2](); // 3, 而不是 2
上述例子就证明了 —— 同一个上下文中创建的闭包是共用一个[[Scope]]属性的。因此上层上下文中的变量“k”是可以很容易就被改变的。
activeContext.Scope = [ ... // higher variable objects {data: [...], k: 3} // activation object ]; data[0].[[Scope]] === Scope; data[1].[[Scope]] === Scope; data[2].[[Scope]] === Scope;
这样一来,在函数激活的时候,最终使用到的k就已经变成了3了。
如下所示,创建一个额外的闭包就可以解决这个问题了:
var data = []; for (var k = 0; k < 3; k++) { data[k] = (function _helper(x) { return function () { alert(x); }; })(k); // 将 "k" 值传递进去 } // 现在就对了 data[0](); // 0 data[1](); // 1 data[2](); // 2
上述例子中,函数“_helper”创建出来之后,通过参数“k”激活。其返回值也是个函数,该函数保存在对应的数组元素中。 这种技术产生了如下效果: 在函数激活时,每次“_helper”都会创建一个新的变量对象,其中含有参数“x”,“x”的值就是传递进来的“k”的值。 这样一来,返回的函数的[[Scope]]就成了如下所示:
data[0].[[Scope]] === [ ... // 更上层的变量对象 上层上下文的AO: {data: [...], k: 3}, _helper上下文的AO: {x: 0} ]; data[1].[[Scope]] === [ ... // 更上层的变量对象 上层上下文的AO: {data: [...], k: 3}, _helper上下文的AO: {x: 1} ]; data[2].[[Scope]] === [ ... // 更上层的变量对象 上层上下文的AO: {data: [...], k: 3}, _helper上下文的AO: {x: 2} ];
我们看到,这个时候函数的[[Scope]]属性就有了真正想要的值了,为了达到这样的目的,我们不得不在[[Scope]]中创建额外的变量对象。 要注意的是,在返回的函数中,如果要获取“k”的值,那么该值还是会是3。
顺便提下,大量介绍JavaScript的文章都认为只有额外创建的函数才是闭包,这种说法是错误的(我也差点有这个错误的认识,再次感谢作者指出。)。 实践得出,这种方式是最有效的,然而,从理论角度来说,在ECMAScript中所有的函数都是闭包。
然而,上述提到的方法并不是唯一的方法。通过其他方式也可以获得正确的“k”的值,如下所示:
var data = []; for (var k = 0; k < 3; k++) { (data[k] = function () { alert(arguments.callee.x); }).x = k; // 将“k”存储为函数的一个属性 } // 同样也是可行的 data[0](); // 0 data[1](); // 1 data[2](); // 2
然而,arguments.callee从ECMAScript (ES5)中移除了,所以这个方法了解下就行了。
FunArg和return另外一个特性是从闭包中返回。在ECMAScript中,闭包中的返回语句会将控制流返回给调用上下文(调用者)。 而在其他语言中,比如,Ruby,有很多中形式的闭包,相应的处理闭包返回也都不同,下面几种方式都是可能的:可能直接返回给调用者,或者在某些情况下——直接从上下文退出。
ECMAScript标准的退出行为如下:
function getElement() { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // 返回给函数"forEach", // 而不会从getElement函数返回 alert('found: ' + element); // found: 2 return element; } }); return null; } alert(getElement()); // null, 而不是 2
然而,在ECMAScript中通过try catch可以实现如下效果(这个方法简直太棒了!):
var $break = {}; function getElement() { try { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // 直接从getElement"返回" alert('found: ' + element); // found: 2 $break.data = element; throw $break; } }); } catch (e) { if (e == $break) { return $break.data; } } return null; } alert(getElement()); // 2
理论版本通常,程序员会错误的认为,只有匿名函数才是闭包。其实并非如此,正如我们所看到的 —— 正是因为作用域链,使得所有的函数都是闭包(与函数类型无关: 匿名函数,FE,NFE,FD都是闭包), 这里只有一类函数除外,那就是通过Function构造器创建的函数,因为其[[Scope]]只包含全局对象。 为了更好的澄清该问题,我们对ECMAScript中的闭包作两个定义(即两种闭包):
ECMAScript中,闭包指的是: