閉包的錯誤使用
作者:HerryLo
原文永久鏈接: https://github.com/AttemptWeb/Record...
尷尬了,遇到了一個閉包的問題,然後我說錯了答案,裝逼失敗了,之前我以爲自己完全理解了閉包,現在發現其實並沒有,趕緊翻書找答案-ing。
看下面的代碼,在循環中向數組導入函數, 希望可以打印 0,1,2 :
function func() {
var arr = [];
for(var i = 0;i<3;i++){
arr.push(()=> {
console.log(i);
})
}
return arr
}
var result = func();
result.forEach((item)=> {
item();
})
<!-- 打印信息 三個3 -->
// 3
發現打印的都是3,原因是匿名函數中的i共享了同一個詞法作用域 。當變量數組調用匿名函數時, var 聲明的變量不存在塊級作用域, i 的值已經指向了for循環 i 的最後一項。
解決上面的問題
使用let或者閉包可以解決上面的問題,解決代碼如下:
<!-- 方案一 -->
// 使用let 聲明變量
function func() {
var arr = [];
for(let i = 0;i<3;i++){
arr.push(()=> {
console.log(i);
})
}
return arr
}
var result = func();
result.forEach((item)=> {
item();
})
<!-- 方案二 -->
// 使用閉包
function func() {
var arr = [];
for(var i = 0;i<3;i++){
(function(){
arr.push(()=> {
console.log(i);
})
})()
}
return arr
}
var result = func();
result.forEach((item)=> {
item();
})
以爲已經解決了, 沒想出了其他問題!可以運行一下上面的方案一和方案二, 你會發現方案二的結果是打印出了三個3,WT?不是應該打印0、1、2, 怎麼沒有?
閉包的作用域鏈
方案一中當然是沒有問題的,使用let解決作用域問題。在方案二中, 使用閉包解決變量i的作用域問題,但是好像閉包失效了。
在方案二中閉包的作用是變量私有化,保存閉包作用域鏈,變量i不被銷燬。對於閉包作用域不瞭解的可以 查看冴羽的 JavaScript深入之執行上下文 和 JavaScript深入之閉包。而方案二中的結果卻不是我們想要的,究其原因,是我對閉包作用域不理解導致的。
<!-- 方案三 -->
function func() {
var arr = [];
for(var i = 0;i<3;i++){
(function(i){
arr.push(()=> {
console.log(i);
})
})(i)
}
return arr
}
var result = func();
result.forEach((item)=> {
item();
})
以上就是解決方案,將i加在匿名函數參數就解決了方案二的問題。
下面來細說一下是怎麼回事: 爲了方便描述,我將 自運行的匿名函數 簡稱爲 fn1, 而arr中的回調匿名函數 簡稱爲 fn2。當然arr中的的三個函數分別是三個不同的 fn2函數。
函數作用域鏈
fn2函數作用域鏈 : {
fn2函數變量和參數 , fn1函數變量&&參數 , func函數變量&&參數 , 全局作用域變量
}
當調用func函數, 函數fn1自運行,變量arr被注入三個fn2函數,同時arr被return出來。此時形成了閉包作用域鏈。
數組中的fn2函數運行向上不斷查找i,fn1函數和fn2函數中都不存在i,直到找到func函數變量i,此時由於i是var聲明的,不存在塊級作用域,三個fn2函數共享i。(其實這個地方有點和最開頭的解釋重複了,不過這裏出現的新東西可能就是閉包作用域鏈了)。
在正常函數調用後,作用域鏈被銷燬,但當存在閉包時,對應的作用域鏈會被保存。 arr中的fn2函數作用域,就基本形成一個作用域鏈,作用域鏈是單向的,內部向外部查找,由下向上查找。作用域鏈中會保存局部變量、全局變量、函數參數。
比較方案二 與 方案三
// 方案二
function func() {
var arr = [];
for(var i = 0;i<3;i++){
(function(){
arr.push(()=> {
console.log(i);
})
})()
}
return arr
}
var result = func();
result.forEach((item)=> {
item();
})
現在再來看 方案二: 遍歷result調用時,arr中的fn2函數會先查找自身函數作用域,不存在i那麼就向上繼續查找,找到func函數下的變量i,將i打印出來,但此時func函數中的變量i已經等於3了, 由於for循環三次,arr中有三個fn2函數, 同上, 所以打印了三次3。
// 方案三
function func() {
var arr = [];
for(var i = 0;i<3;i++){
(function(i){
arr.push(()=> {
console.log(i);
})
})(i)
}
return arr
}
var result = func();
result.forEach((item)=> {
item();
})
而在 方案三: 遍歷result調用時, arr中的fn2函數會先查找自身函數作用域,不存在i那麼就向上繼續查找,找到fn1函數中參數i, 將i打印出來。 由於存在三個匿名函數,所以函數參數分別是0、1、2。
結尾: 其實說到這裏,基本可以瞭解,方案二中的問題,其實就是閉包作用域鏈的問題,當形成閉包時,閉包涉及到的作用域鏈會被保存。如果真正的瞭解了閉包,絕對不會遇見像我這樣的問題,算是給我自己上了一課。寫的不好的地方希望大家可以指出,下面是參考的文章鏈接。
(如果上面的內容引起你的不適感,可以參考 冴羽的blog )
參考文章:
ps: 順便推一下自己的個人公衆號:Yopai,有興趣的可以關注,每週不定期更新,分享可以增加世界的快樂