JS 閉包的實現(xiàn)原理

函數(shù)都有自己的執(zhí)行環(huán)境,該環(huán)境定義了變量或者函數(shù)訪問數(shù)據(jù)的權(quán)限,當離開執(zhí)行環(huán)境后,該環(huán)境內(nèi)的變量會被銷毀。

function add() {
    let a = 1;
    console.log(a); // 1
}
console.log(a); // ReferenceError: a is not defined

上例a在 add() 函數(shù)的作用域內(nèi),能夠訪問,離開作用域后,就無法訪問了。

那有沒有辦法在 add() 函數(shù)外訪問a的值呢?

function add() {
    const a = 1;
    const addOne = function(b) { return b + a; }
    return addOne;
}

const addOne = add();
console.log(addOne(1)); // 2

add() 函數(shù)執(zhí)行完畢之后,會從函數(shù)調(diào)用棧中被推出,同時局部變量a應(yīng)該被清理才對, 但我們調(diào)用 addOne(1) ,得到的結(jié)果卻是2。說明a在 add() 執(zhí)行結(jié)束后并沒有被銷毀,而是進入到了 addOne() 的作用域。

這里的 addOne() 函數(shù)被稱為匿名函數(shù)(anonymous function)也叫做閉包(closure)。

那么 JS 的閉包是如何實現(xiàn)對外部變量的存儲的呢?

作用域鏈

上面的例子中 addOne 函數(shù)獲得a的值,我們需要先弄清楚,它是拷貝了a的值到其作用域中,還是a沒有壓根被銷毀,而是給了 addOne 訪問的權(quán)限。

function add() {
    let a = 1;
    const addOne = function(b) { return b + a; }
    ++a;
    return addOne;
}
const addOne = add();
console.log(addOne(1)); // 3

我們可以發(fā)現(xiàn)在a被 addOne 捕獲之后,再修改它的值, addOne() 的執(zhí)行結(jié)果也隨之變化,所以閉包不是拷貝變量的值,而是持有它的一個「引用」。

JS 在運行的時候,會為每一個執(zhí)行函數(shù)分配內(nèi)存空間,我們稱這個空間為作用域?qū)ο螅⊿cope Object)。當調(diào)用函數(shù)時,函數(shù)中分配的本地變量會被存儲在這個作用域?qū)ο笾?。我們無法直接使用代碼讀取這個作用域?qū)ο?,但是解析器在處理?shù)據(jù)的時候會在后臺使用它們。

JS 的函數(shù)是一等公民,聲明時可能會有嵌套關(guān)系,因此會同時存在多個作用域?qū)ο?,所有函?shù)的作用域?qū)ο髸?strong>環(huán)境棧所管理。環(huán)境棧中的作用域?qū)ο笫前错樞蛟L問的,最先能夠訪問的是當前函數(shù)的作用域,如果訪問的變量在當前作用域沒有,會訪問上一層作用域,直到找到全局作用域(Global)對象。如果訪問到全局作用域也沒有這個對象,會拋出ReferenceError的異常。這就是所謂的作用域鏈(scope chian)。

這跟原型繼承的概念非常相似,只不過在原型鏈頂端找到不到某個屬性時,返回是undefined。

閉包之所以能夠訪問到上一層函數(shù)中的局部變量,是因為當變量被捕獲之后,即使上一層函數(shù)調(diào)用完畢出棧了,但是它的作用域?qū)ο鬀]有被銷毀,所以仍然能夠被閉包訪問。

驗證

上面的理論是否正確,我們通過代碼來驗證一下。

function add(a) {
    const addB = function(b) {
        console.log(b);
        const addC = function(c) {
            return a + b + c;
        }
        return addC;
    }
    return addB;
}

const addOne = add(1);
const addTwo = addOne(1);
const addThree = addTwo(1);

這個例子中嵌套了兩層閉包,我們分別在第 3 行和第 5 行打上斷點。當斷住第 3 行代碼時,debuger 區(qū)域展示了 addB() 函數(shù)的作用域?qū)ο?Scope。


F79B1CBF-89A7-433E-9F9F-4AC75896D658.png

我們可以看到一個 Closure 屬性,它持有了 add() 函數(shù)作用域中的 a,說明 Closure 對象存儲了從外部捕獲的變量。

當斷住第 5 行進入下一層閉包,出現(xiàn)了兩個 Closure 對象。


A6E3B43C-92AE-41E7-B014-6069400306B1.png

從中可以看出,閉包的作用域?qū)ο髸鶕?jù)外部函數(shù)的層級,生成對應(yīng)的對象屬性來存儲變量。假如我們在 addB 函數(shù)中創(chuàng)建一個局部變量 a,覆蓋從 add 函數(shù)的捕獲的局部,對應(yīng)的 Closure(add) 屬性就不會生成了。

鬼畜的 var 關(guān)鍵字

JS 作用域?qū)ο蟮脑O(shè)計顛覆了我以往對程序語言的認知,比如說下面這段代碼,a 明明是個局部變量,但是在 if 語句外面卻能被訪問到它。

function testVar() {
    if (1) {
        var a = 1;
            let b = 2;
    }
    console.log(a); // 1
    console.log(b); // ReferenceError
}
testVar();
7C2A2E8B-7BFC-49CE-8DAB-969842F86E69.png

var 聲明的變量作用域會被提升到 Local,也就是當前函數(shù)的局部作用域中,而 let 聲明的變量保存在 Block 屬性中,當 if 語句執(zhí)行結(jié)束,這個 Block 會被銷毀,所以在 if 外就無法訪問到 b 變量。

最后我們來看一段有趣的代碼,思考一下輸出結(jié)果會是什么。

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]);

    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

testList() 

輸出結(jié)果為:

"item2 undefined"
"item2 undefined"
"item2 undefined"

我們使用循環(huán)將三個閉包存入數(shù)組,每次循環(huán)閉包都會捕獲了數(shù)組的下標 i,照理說再取出閉包執(zhí)行的時候,會順序輸出數(shù)組的元素,實際上卻沒有。

這是因為 var 關(guān)鍵字搞得鬼,被 var 關(guān)鍵字聲明的 i 和 item 變量,作用域被提升到 buildList 函數(shù)的整個作用域(Local)中,每次循環(huán)它們的值都會被覆蓋。對于同一層級的變量,閉包只會持有它的一個“引用”,所以在它執(zhí)行時只能訪問到的 i 和 item 的最新值 i = 3,item = item2。

如果將 i 和 item 都用 let 修飾,閉包捕獲的變量會存入閉包作用域?qū)ο蟮?Block 屬性,每次循環(huán) Block 屬性會被重新創(chuàng)建,并被數(shù)組持有。所以,遍歷數(shù)組能輸出預(yù)期的結(jié)果。

item0 1
item1 2
item2 3

總結(jié)

閉包能夠訪問外部函數(shù)的變量,即使變量已經(jīng)離開它所創(chuàng)建的環(huán)境,是因為外部變量會被閉包的作用域?qū)ο笏钟小i]包這種特性實現(xiàn)了嵌套函數(shù)之間數(shù)據(jù)的隱式傳遞。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ● 閉包基礎(chǔ) ● 閉包作用 ● 閉包經(jīng)典例子 ● 閉包應(yīng)用 ● 閉包缺點 ● 參考資料 1、閉包基礎(chǔ) 作用域和作...
    lzyuan閱讀 1,047評論 0 0
  • 作用域和閉包是 JavaScript 最重要的概念之一,想要進一步學(xué)習(xí) JavaScript,就必須理解 Java...
    劼哥stone閱讀 1,249評論 1 13
  • 找到片源很迫不及待的看,整體色調(diào)淡而不抑,很適合晚上舒服的躺著看。因為知道不會失望,但也期待會將這么措手不...
    29c59f1aa9a3閱讀 999評論 0 0
  • 她,胖胖的,圓圓的臉活像一根肉鼓鼓的香腸,緊繃繃的,從中又帶有點點紅色,她,就是沈同學(xué)。她還有一個特點,有暴脾氣...
    貝殼白白閱讀 297評論 0 0
  • 重新修正過的21天微目標 學(xué)習(xí)目標:1:通過給自己每天制定的計劃完成《時間管理目標模型》課程 2...
    煙雨十里燈火閱讀 268評論 0 0

友情鏈接更多精彩內(nèi)容