閉包的錯誤使用

HerryLo發表於2018-12-27

 
 
 
尷尬了,遇到了一個閉包的問題,然後我說錯了答案,裝逼失敗了,之前我以為自己完全理解了閉包,現在發現其實並沒有,趕緊翻書找答案-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函式,同時arrreturn出來。此時形成了閉包作用域鏈

陣列中的fn2函式執行向上不斷查詢i,fn1函式和fn2函式中都不存在i,直到找到func函式變數i,此時由於ivar宣告的,不存在塊級作用域,三個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

參考文章:

MDN 閉包

JavaScript深入之執行上下文

JavaScript深入之閉包

來源:https://juejin.im/post/5c22f13b5188252b56273a00

相關文章