引言
網路上關於作用域及閉包的文章很多,自己對於純理論知識並不能很快的理解,但自己對於圖畫有很強的記憶和理解能力,因此決定將此知識點以圖畫的知識表現出來,加深自身理解的同時如果能幫到正在學習的童鞋就再好不過了
下面我以函式的整個生命週期來訴說此部分知識
函式生命週期
先寫一下示例程式碼
var a = 10;
function func(a) {
var a = 20;
a++;
console.log(a);
}
func();
console.log(a);
複製程式碼
開始執行程式前
-
先建立 ECS,ECS 其實就是專門儲存正在呼叫的函式的執行環境的陣列,也可以說物件,其實關聯陣列也就相當於物件。
-
然後在 ECS 中新增瀏覽器主程式的執行環境 main
-
建立全域性作用域物件 window
-
main 執行環境引用 window
定義函式時
-
原始型別的全域性變數會直接存入 window 環境當中,因為函式是引用型別,所以首先用函式名宣告全域性變數
-
然後建立函式物件,封裝函式定義
-
函式物件的 scope 屬性,指回函式建立時的作用域,意思是,函式執行時如果函式本身提供的變數不能讓函式執行完全,那它便會去回它建立時的那個作用域去尋找變數。
-
函式名後面存入指向函式物件的地址
引用型別在其中只能儲存地址,這個在此筆記談談值傳遞中有詳細說明
函式呼叫時
-
向 ECS 中壓入本次函式呼叫的執行環境元素
-
建立本次函式呼叫時使用的函式作用域物件(AO),也就是臨時作用域
-
在 AO 中建立儲存所有的區域性變數,包括形參變數和函式內用 var 宣告的變數
-
設定 AO 的 parent 屬性和引用函式的 scope 屬性指向父級作用域物件
-
函式的執行環境引用 AO
-
順著那個箭頭,先在 AO 中找變數,也就是區域性變數,如果 AO 中沒有,再順著箭頭去父級作用域中找
函式呼叫後
函式的執行環境出棧,AO 釋放,AO 中的區域性變數一同被釋放掉。
我們得知整個結果之後,自然而然那兩個 console
的結果也顯然意見。
閉包
前面我們提到過,全域性變數是可重用但是汙染全域性,區域性變數不會汙染全域性但是不可重用。
我自己認為閉包就是重用變數又保護變數不被汙染的機制,就是為了解決這一情況而生的。
特點
包裹受保護的變數和操作變數的內層函式的外層函式
外層函式要返回內層函式的物件
return function(){..}
- 直接給全域性變數賦值一個內部
function
- 將內部函式儲存在一個物件的屬性或陣列元素中
return [function function function]
或return {fun:function(){...}}
呼叫外層函式,用外部變數接住返回的內層函式物件,形成閉包。
原理
先貼出示例程式碼
function outer() {
var num = 1;
return function() {
console.log(num++);
};
}
var getNum = outer();
getNum();
getNum();
num = 1;
getNum();
複製程式碼
下面我把閉包形成的原理用畫圖工具畫出來
window 中存入 outer 名並指向 outer 函式物件,getNum
因為宣告提前也先將變數名存在 window 中。
getNum = outer()
其實包含 outer 的建立和 getNum
的賦值。
上面的圖畫的是 outer 函式進行到 var num = 1;
,前面都有說過,不過多重複。
建立了匿名函式,getNum
指向了匿名函式物件,匿名物件的 scope 指向它的父級作用域,也就是 outer 的作用域,那這樣就形成了圖中的三角關係,此時 outer 執行完畢,離開 ECS 執行環境,outer 的 AO 本也應該隨著離開,但是因為這強大的三角關係,強行拉住不讓其釋放,也就形成了所謂的閉包。
那其實閉包的原因就是:外層函式的作用域物件無法釋放
getNum=outer()
getNum 其實就是一個函式
呼叫getNum()
,會生成 getNum
的臨時作用域,圖中可看出,getNum
其實就是在 outer 中的匿名函式,所以他的 parent 就指向 outer 留下的作用域。當他執行 console.log(num++)
的時候,在他的作用域中沒有 num
變數他就會順著作用域鏈去尋找,最終在 outer 中的作用域中找到 num
並對其進行自加操作。所以當下次呼叫 getNum 的時候 num 會從 2 開始,不會是一開始的 1。
num 不是全域性變數,還實現了 num 變數的重複呼叫。就達到了閉包的目的。
設定 num = 1
只是在 window
物件上新增儲存 num
的值,當下次呼叫 getNum
的時候 js 引擎還會從 getNum
作用域開始順著作用域鏈尋找 num
,在 outerAO 就會尋找到 num
,所以根本不會影響到 window 中的 num
,也不會受其影響。因此此段程式碼輸出的結果為 1 2 3
。
缺點
當然閉包也有其缺點
-
比普通函式佔用更多記憶體,因為外層函式的作用域物件(AO)始終存在
-
容易造成記憶體洩漏
解決辦法
將引用記憶體函式物件的外部變數重置為 null
getNum = null;
複製程式碼
getNum 指向 outer 函式物件的那根線就會斷掉,三角關係破裂,那函式物件和 outerAO 也會相繼被銷燬。
覺得文章不錯的話還請各位大佬給個 star 鼓勵一下 gayhub