被閉包啪啪啪的打臉之閉包錯誤使用

閉包的錯誤使用

作者: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 )

參考文章:

MDN 閉包

JavaScript深入之執行上下文

JavaScript深入之閉包

ps: 順便推一下自己的個人公衆號:Yopai,有興趣的可以關注,每週不定期更新,分享可以增加世界的快樂

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章