在文章的开头,简单来提一下,什么是闭包。
JavaScript中的闭包,就像一个副本,将某函数在退出时候的所有局部变量复制保存其中。 这些函数可以“记忆”他被创建时候的上下文环境,将外部变量保留在栈帧中。
首先,我们来看这样一个例子:
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
// 使用j是为了防止搞混---可以使用i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //输出 "item2 undefined" 3 次
在上面这个例子中,console会输出三次“item2 undefined”
为什么没有按照for循环的顺序,输出
item0
,item1
,item2
的值呢?这是因为在上述代码中,for循环里产生了一个闭包,当
result.push( function() {console.log(item + ' ' + list[i])} );
这行代码运行时,由于i
变量并没有在这个无名函数中定义,所以会到上层语义环境中去找。此时,for循环并不会因此中断,等待无名函数。那么当无名函数在上层语义环境中找到i
的值,这是for循环已经结束,i
的值自然变成了3,而item
此时的值则为item2
.那么在接下来的for循环中
function testList() {
var fnlist = buildList([1,2,3]);
// 使用j是为了防止搞混---可以使用i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
实际上时运行了三次console.log('item2' + ' ' + list[3])
,由于传入函数bulidList
的数组为[1, 2, 3]
,list[3]
的值类型自然是undefined
。
若是我们将传入的数组做出一点修改,就会发现,console会将list[3]
的值正确输出。第一个for循环中的判断条件由i < list.length
改为i < 3
,传入的数组由[1, 2, 3]
改为[1, 2, 3, 4]
。
function buildList(list) {
var result = [];
for (var i = 0; i < 3; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1, 2, 3, 4]);
// 使用j是为了防止搞混---可以使用i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //输出 "item2 4" 3 次
如果在for循环中产生了闭包,我们如何让它输出我们想要的结果呢?再来看一个例子吧。
var list = document.getElementById("list");
//插入五个<li>标签
for ( i = 1; i <= 5; i++) {
var item = document.createElement("LI");
item.appendChild(document.createTextNode("Item " + i));
//分别为五个<li>标签绑定onclick事件
item.onclick = function (ev) {
console.log("Item " + i + " is clicked.");
};
list.appendChild(item);
}
这个例子中,我们想要的效果是点击不同的<li>
标签,console会输出对应的Itemi is cilick。但是由于for循环里面产生了闭包,实际的结果是无论点击哪个<li>
,console输出的都是Item 6 is clicked.
如果我们将代码稍作修改,再增加一层闭包,并将i
作为参数传入到函数中,我们将会得到正确地输出。
for ( i = 1; i <= 5; i++) {
var item = document.createElement("LI");
item.appendChild(document.createTextNode("Item " + i));
(function(i){
item.onclick = function (ev) {
console.log("Item " + i + " is clicked.");
};
list.appendChild(item);
})(i);
}
这段代码中,虽然
console.log("Item " + i + " is clicked.");
仍然需要去上层语义环境中找i
的值,但是由于外面增加了一个function
,并将i
作为参数传入,此时便可以寻找到正确地值。在这里,每一次for循环,都会产生一个大的闭包,实际上到循环结束,共产生了五个闭包,这五个闭包里面分别存储了i
从1-5的五个值。如果我们不将i
作为参数传入会是什么样的?
for ( i = 1; i <= 5; i++) {
var item = document.createElement("LI");
item.appendChild(document.createTextNode("Item " + i));
(function(){
item.onclick = function (ev) {
console.log("Item " + i + " is clicked.");
};
list.appendChild(item);
})();
}
可以看到,跟之前没有在外层套上函数时是一样的输出。那么有没有什么办法不传
i
作为参数也可以得到正确的输出呢?有的!看下面的代码。
for ( i = 1; i <= 5; i++) {
var item = document.createElement("LI");
item.appendChild(document.createTextNode("Item " + i));
(function(){
var j = i;
item.onclick = function (ev) {
console.log("Item " + j + " is clicked.");
};
list.appendChild(item);
})();
}
这段代码中,我增加了var j = i;
,并将之前的i
改为j
。此时,已经能够得到正确的输出。
为什么增加了一个var j = i
就可以得到正确的输出了呢?实际上原理和上面并没有变化,主要是因为这里产生了五个闭包,每一个闭包里面的j
都引用了一个i
值。但再稍加修改,就又会不同。接下来,我将var j = i;
改为j = i;
,看看会有什么变化。
这里的输出又出错了,会得到五个同样的输出。但是请注意了虽然同是同样的输出,却与之前略有不同。这里的五个输出都是Item 5 is clicked.而之前则是Item 6 is clicked.
得到错误的输出是因为去掉了关键字
var
之后,j
的作用域发生了变化,成为了全局变量。五个j
引用了同一个i
值。至于为什么是5而不是6,则是因为j
引用的是最后一次循环时的i
值,而不是循环结束以后的i
值。最后,这篇文章中的内容,是我在看ES6标准中
let
关键字相关的内容时想到的。那么你肯定会问了,是不是 let
也可以解决for循环中闭包的问题?Bingo!再来看看下面的代码吧。
for ( i = 1; i <= 5; i++) {
var item = document.createElement("LI");
item.appendChild(document.createTextNode("Item " + i));
let j = i;
item.onclick = function (ev) {
console.log("Item " + j + " is clicked.");
};
list.appendChild(item);
}
这里let
创建的变量j
是拥有块级作用域的,在ES6之前js是没有块级作用域的。
当然,解决办法还有很多,你觉得哪种办法最优雅呢?