圖解作用域及閉包

Hutchins發表於2018-05-08

引言

網路上關於作用域及閉包的文章很多,自己對於純理論知識並不能很快的理解,但自己對於圖畫有很強的記憶和理解能力,因此決定將此知識點以圖畫的知識表現出來,加深自身理解的同時如果能幫到正在學習的童鞋就再好不過了

下面我以函式的整個生命週期來訴說此部分知識

函式生命週期

先寫一下示例程式碼

var a = 10;
function func(a) {
  var a = 20;
  a++;
  console.log(a);
}
func();
console.log(a);
複製程式碼

開始執行程式前

pic2

  1. 先建立 ECS,ECS 其實就是專門儲存正在呼叫的函式的執行環境的陣列,也可以說物件,其實關聯陣列也就相當於物件。

  2. 然後在 ECS 中新增瀏覽器主程式的執行環境 main

  3. 建立全域性作用域物件 window

  4. main 執行環境引用 window

定義函式時

pic3

  1. 原始型別的全域性變數會直接存入 window 環境當中,因為函式是引用型別,所以首先用函式名宣告全域性變數

  2. 然後建立函式物件,封裝函式定義

  3. 函式物件的 scope 屬性,指回函式建立時的作用域,意思是,函式執行時如果函式本身提供的變數不能讓函式執行完全,那它便會去回它建立時的那個作用域去尋找變數。

  4. 函式名後面存入指向函式物件的地址

引用型別在其中只能儲存地址,這個在此筆記談談值傳遞中有詳細說明

函式呼叫時

pic4

  1. 向 ECS 中壓入本次函式呼叫的執行環境元素

  2. 建立本次函式呼叫時使用的函式作用域物件(AO),也就是臨時作用域

  3. 在 AO 中建立儲存所有的區域性變數,包括形參變數和函式內用 var 宣告的變數

  4. 設定 AO 的 parent 屬性和引用函式的 scope 屬性指向父級作用域物件

  5. 函式的執行環境引用 AO

  6. 順著那個箭頭,先在 AO 中找變數,也就是區域性變數,如果 AO 中沒有,再順著箭頭去父級作用域中找

函式呼叫後

pic5

函式的執行環境出棧,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();
複製程式碼

下面我把閉包形成的原理用畫圖工具畫出來

pic6

window 中存入 outer 名並指向 outer 函式物件,getNum 因為宣告提前也先將變數名存在 window 中。

getNum = outer() 其實包含 outer 的建立和 getNum 的賦值。

上面的圖畫的是 outer 函式進行到 var num = 1; ,前面都有說過,不過多重複。

pic7

建立了匿名函式,getNum 指向了匿名函式物件,匿名物件的 scope 指向它的父級作用域,也就是 outer 的作用域,那這樣就形成了圖中的三角關係,此時 outer 執行完畢,離開 ECS 執行環境,outer 的 AO 本也應該隨著離開,但是因為這強大的三角關係,強行拉住不讓其釋放,也就形成了所謂的閉包。

那其實閉包的原因就是:外層函式的作用域物件無法釋放

pic8

getNum=outer()getNum 其實就是一個函式

pic9

呼叫getNum(),會生成 getNum 的臨時作用域,圖中可看出,getNum 其實就是在 outer 中的匿名函式,所以他的 parent 就指向 outer 留下的作用域。當他執行 console.log(num++) 的時候,在他的作用域中沒有 num 變數他就會順著作用域鏈去尋找,最終在 outer 中的作用域中找到 num 並對其進行自加操作。所以當下次呼叫 getNum 的時候 num 會從 2 開始,不會是一開始的 1

num 不是全域性變數,還實現了 num 變數的重複呼叫。就達到了閉包的目的。

pic10

設定 num = 1 只是在 window 物件上新增儲存 num 的值,當下次呼叫 getNum 的時候 js 引擎還會從 getNum 作用域開始順著作用域鏈尋找 num,在 outerAO 就會尋找到 num,所以根本不會影響到 window 中的 num,也不會受其影響。因此此段程式碼輸出的結果為 1 2 3

缺點

當然閉包也有其缺點

  • 比普通函式佔用更多記憶體,因為外層函式的作用域物件(AO)始終存在

  • 容易造成記憶體洩漏

解決辦法

將引用記憶體函式物件的外部變數重置為 null

getNum = null;
複製程式碼

pic11

getNum 指向 outer 函式物件的那根線就會斷掉,三角關係破裂,那函式物件和 outerAO 也會相繼被銷燬。

覺得文章不錯的話還請各位大佬給個 star 鼓勵一下 gayhub

相關文章