在文章的開(kāi)頭陈肛,簡(jiǎn)單來(lái)提一下揍鸟,什么是閉包。
JavaScript中的閉包,就像一個(gè)副本阳藻,將某函數(shù)在退出時(shí)候的所有局部變量復(fù)制保存其中晰奖。 這些函數(shù)可以“記憶”他被創(chuàng)建時(shí)候的上下文環(huán)境,將外部變量保留在棧幀中腥泥。
首先匾南,我們來(lái)看這樣一個(gè)例子:
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 次
在上面這個(gè)例子中,console會(huì)輸出三次“item2 undefined”
為什么沒(méi)有按照f(shuō)or循環(huán)的順序蛔外,輸出
item0
蛆楞,item1
,item2
的值呢夹厌?這是因?yàn)樵谏鲜龃a中豹爹,for循環(huán)里產(chǎn)生了一個(gè)閉包,當(dāng)
result.push( function() {console.log(item + ' ' + list[i])} );
這行代碼運(yùn)行時(shí)矛纹,由于i
變量并沒(méi)有在這個(gè)無(wú)名函數(shù)中定義臂聋,所以會(huì)到上層語(yǔ)義環(huán)境中去找。此時(shí)或南,for循環(huán)并不會(huì)因此中斷孩等,等待無(wú)名函數(shù)。那么當(dāng)無(wú)名函數(shù)在上層語(yǔ)義環(huán)境中找到i
的值采够,這是for循環(huán)已經(jīng)結(jié)束肄方,i
的值自然變成了3,而item
此時(shí)的值則為item2
.那么在接下來(lái)的for循環(huán)中
function testList() {
var fnlist = buildList([1,2,3]);
// 使用j是為了防止搞混---可以使用i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
實(shí)際上時(shí)運(yùn)行了三次console.log('item2' + ' ' + list[3])
蹬癌,由于傳入函數(shù)bulidList
的數(shù)組為[1, 2, 3]
权她,list[3]
的值類型自然是undefined
。
若是我們將傳入的數(shù)組做出一點(diǎn)修改冀瓦,就會(huì)發(fā)現(xiàn)伴奥,console會(huì)將list[3]
的值正確輸出。第一個(gè)for循環(huán)中的判斷條件由i < list.length
改為i < 3
翼闽,傳入的數(shù)組由[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循環(huán)中產(chǎn)生了閉包,我們?nèi)绾巫屗敵鑫覀兿胍慕Y(jié)果呢感局?再來(lái)看一個(gè)例子吧尼啡。
var list = document.getElementById("list");
//插入五個(gè)<li>標(biāo)簽
for ( i = 1; i <= 5; i++) {
var item = document.createElement("LI");
item.appendChild(document.createTextNode("Item " + i));
//分別為五個(gè)<li>標(biāo)簽綁定onclick事件
item.onclick = function (ev) {
console.log("Item " + i + " is clicked.");
};
list.appendChild(item);
}
這個(gè)例子中,我們想要的效果是點(diǎn)擊不同的<li>
標(biāo)簽询微,console會(huì)輸出對(duì)應(yīng)的Itemi is cilick崖瞭。但是由于for循環(huán)里面產(chǎn)生了閉包,實(shí)際的結(jié)果是無(wú)論點(diǎn)擊哪個(gè)<li>
撑毛,console輸出的都是Item 6 is clicked.
如果我們將代碼稍作修改书聚,再增加一層閉包,并將i
作為參數(shù)傳入到函數(shù)中,我們將會(huì)得到正確地輸出雌续。
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.");
仍然需要去上層語(yǔ)義環(huán)境中找i
的值,但是由于外面增加了一個(gè)function
驯杜,并將i
作為參數(shù)傳入受啥,此時(shí)便可以尋找到正確地值。在這里鸽心,每一次for循環(huán)滚局,都會(huì)產(chǎn)生一個(gè)大的閉包,實(shí)際上到循環(huán)結(jié)束顽频,共產(chǎn)生了五個(gè)閉包藤肢,這五個(gè)閉包里面分別存儲(chǔ)了i
從1-5的五個(gè)值。如果我們不將i
作為參數(shù)傳入會(huì)是什么樣的糯景?
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);
})();
}
可以看到谤草,跟之前沒(méi)有在外層套上函數(shù)時(shí)是一樣的輸出。那么有沒(méi)有什么辦法不傳
i
作為參數(shù)也可以得到正確的輸出呢莺奸?有的!看下面的代碼冀宴。
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
略贮。此時(shí)甚疟,已經(jīng)能夠得到正確的輸出。
為什么增加了一個(gè)var j = i
就可以得到正確的輸出了呢逃延?實(shí)際上原理和上面并沒(méi)有變化览妖,主要是因?yàn)檫@里產(chǎn)生了五個(gè)閉包,每一個(gè)閉包里面的j
都引用了一個(gè)i
值揽祥。但再稍加修改讽膏,就又會(huì)不同。接下來(lái)拄丰,我將var j = i;
改為j = i;
府树,看看會(huì)有什么變化。
這里的輸出又出錯(cuò)了料按,會(huì)得到五個(gè)同樣的輸出奄侠。但是請(qǐng)注意了雖然同是同樣的輸出,卻與之前略有不同载矿。這里的五個(gè)輸出都是Item 5 is clicked.而之前則是Item 6 is clicked.
得到錯(cuò)誤的輸出是因?yàn)槿サ袅岁P(guān)鍵字
var
之后垄潮,j
的作用域發(fā)生了變化,成為了全局變量。五個(gè)j
引用了同一個(gè)i
值弯洗。至于為什么是5而不是6旅急,則是因?yàn)?code>j引用的是最后一次循環(huán)時(shí)的i
值,而不是循環(huán)結(jié)束以后的i
值涂召。最后坠非,這篇文章中的內(nèi)容,是我在看ES6標(biāo)準(zhǔn)中
let
關(guān)鍵字相關(guān)的內(nèi)容時(shí)想到的果正。那么你肯定會(huì)問(wèn)了炎码,是不是 let
也可以解決for循環(huán)中閉包的問(wèn)題?Bingo秋泳!再來(lái)看看下面的代碼吧潦闲。
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
創(chuàng)建的變量j
是擁有塊級(jí)作用域的,在ES6之前js是沒(méi)有塊級(jí)作用域的迫皱。
當(dāng)然歉闰,解決辦法還有很多,你覺(jué)得哪種辦法最優(yōu)雅呢卓起?