JS 閉包的實現原理

2021-10-09 10:51:01 字數 3346 閱讀 3254

函式都有自己的執行環境,該環境定義了變數或者函式訪問資料的許可權,當離開執行環境後,該環境內的變數會被銷毀。

function add() 

console.log(a); // referenceerror: a is not defined

上例a在 add() 函式的作用域內,能夠訪問,離開作用域後,就無法訪問了。

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

function add() 

return addone;

}const addone = add();

console.log(addone(1)); // 2

add() 函式執行完畢之後,會從函式呼叫棧中被推出,同時區域性變數a應該被清理才對, 但我們呼叫 addone(1) ,得到的結果卻是2。說明a在 add() 執行結束後並沒有被銷毀,而是進入到了 addone() 的作用域。

這裡的 addone() 函式被稱為匿名函式(anonymous function)也叫做閉包(closure)。

那麼 js 的閉包是如何實現對外部變數的儲存的呢?

上面的例子中 addone 函式獲得a的值,我們需要先弄清楚,它是拷貝了a的值到其作用域中,還是a沒有壓根被銷毀,而是給了 addone 訪問的許可權。

function add() 

++a;

return addone;

}const addone = add();

console.log(addone(1)); // 3

我們可以發現在a被 addone 捕獲之後,再修改它的值, addone() 的執行結果也隨之變化,所以閉包不是拷貝變數的值,而是持有它的乙個「引用」。

js 在執行的時候,會為每乙個執行函式分配記憶體空間,我們稱這個空間為作用域物件(scope object)。當呼叫函式時,函式中分配的本地變數會被儲存在這個作用域物件中。我們無法直接使用**讀取這個作用域物件,但是解析器在處理資料的時候會在後台使用它們。

js 的函式是一等公民,宣告時可能會有巢狀關係,因此會同時存在多個作用域物件,所有函式的作用域物件會被環境棧所管理。環境棧中的作用域物件是按順序訪問的,最先能夠訪問的是當前函式的作用域,如果訪問的變數在當前作用域沒有,會訪問上一層作用域,直到找到全域性作用域(global)物件。如果訪問到全域性作用域也沒有這個物件,會丟擲referenceerror的異常。這就是所謂的作用域鏈(scope chian)。

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

閉包之所以能夠訪問到上一層函式中的區域性變數,是因為當變數**獲之後,即使上一層函式呼叫完畢出棧了,但是它的作用域物件沒有被銷毀,所以仍然能夠被閉包訪問。

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

function add(a) 

return addc;

}return addb;

}const addone = add(1);

const addtwo = addone(1);

const addthree = addtwo(1);

這個例子中巢狀了兩層閉包,我們分別在第 3 行和第 5 行打上斷點。當斷住第 3 行**時,debuger 區域展示了 addb() 函式的作用域物件 scope。

f79b1cbf-89a7-433e-9f9f-4ac75896d658.png

我們可以看到乙個 closure 屬性,它持有了 add() 函式作用域中的 a,說明 closure 物件儲存了從外部捕獲的變數。

當斷住第 5 行進入下一層閉包,出現了兩個 closure 物件。

a6e3b43c-92ae-41e7-b014-6069400306b1.png

從中可以看出,閉包的作用域物件會根據外部函式的層級,生成對應的物件屬性來儲存變數。假如我們在 addb 函式中建立乙個區域性變數 a,覆蓋從 add 函式的捕獲的區域性,對應的 closure(add) 屬性就不會生成了。

js 作用域物件的設計顛覆了我以往對程式語言的認知,比如說下面這段**,a 明明是個區域性變數,但是在 if 語句外面卻能被訪問到它。

function testvar() 

console.log(a); // 1

console.log(b); // referenceerror

}testvar();

7c2a2e8b-7bfc-49ce-8dab-969842f86e69.png

var 宣告的變數作用域會被提公升到 local,也就是當前函式的區域性作用域中,而 let 宣告的變數儲存在 block 屬性中,當 if 語句執行結束,這個 block 會被銷毀,所以在 if 外就無法訪問到 b 變數。

最後我們來看一段有趣的**,思考一下輸出結果會是什麼。

function buildlist(list) );

}return result;

}function testlist()

}testlist()

輸出結果為:

"item2 undefined"

"item2 undefined"

"item2 undefined"

我們使用迴圈將三個閉包存入陣列,每次迴圈閉包都會捕獲了陣列的下標 i,照理說再取出閉包執行的時候,會順序輸出陣列的元素,實際上卻沒有。

這是因為 var 關鍵字搞得鬼,被 var 關鍵字宣告的 i 和 item 變數,作用域被提公升到 buildlist 函式的整個作用域(local)中,每次迴圈它們的值都會被覆蓋。對於同一層級的變數,閉包只會持有它的乙個「引用」,所以在它執行時只能訪問到的 i 和 item 的最新值 i = 3,item = item2。

如果將 i 和 item 都用 let 修飾,閉包捕獲的變數會存入閉包作用域物件的 block 屬性,每次迴圈 block 屬性會被重新建立,並被陣列持有。所以,遍歷陣列能輸出預期的結果。

item0 1

item1 2

item2 3

閉包能夠訪問外部函式的變數,即使變數已經離開它所建立的環境,是因為外部變數會被閉包的作用域物件所持有。閉包這種特性實現了巢狀函式之間資料的隱式傳遞。

JS閉包原理分析

js閉包原理分析 閉包是指有權訪問另乙個函式作用域中的變數的函式。在乙個函式a內部返回另乙個函式b,並且函式b裡面使用者a函式內部的私有變數,此外在函式a外面定義乙個變數接收函式b,這樣就形成了閉包。比如在一棟房子裡面有很多個小房間,你手上通過拿著乙個小房間的位址 棟001房 那麼通過小房間的位址就...

js閉包函式原理

變數作用域 全域性變數和區域性變數 問題 函式外部無法讀取內部 宣告也會體現是否全域性在函式內或者外部 1.閉包 就是指能夠讀取其他函式內部變數的函式。在js中,只有函式內部的子函式才能讀取區域性變數,所以閉包可以理解成 定義在乙個函式內部的函式 巢狀函式 本質上,閉包是將函式內部和外部連線起來的橋...

Js中的閉包原理

要了解清楚js中的閉包制機,那麼得先了解全域性執行環境 塊級執行環境 函式執行環境 變數物件 環境棧 作用域鏈 摧毀執行環境。全域性執行環境 全域性執行環境指的是最外層的執行環境。在web中全域性執行環境被認為 window 物件,所以你在全域性環境中建立的變數與函式都是物件的屬性和方法。函式執行環...