用垃圾回收機制解釋JavaScript中的閉包

_嗚啦啦啦火車笛發表於2019-01-30

說起javascript中的閉包,首先要知道為什麼會存在閉包,其作用又是什麼。且為什麼閉包中就能讓外層函式的變數始終儲存呢?下面我們將從這兩個角度去剖析它。當然,大神繞道,謝謝哈。

開門見山,直接總結閉包的兩大核心作用:

  1. 讀取函式內部的變數;
  2. 讓變數始終儲存在記憶體中。

一、讀取函式內部的變數

眾所周知,變數在javascript中有全域性變數和區域性變數,函式內部可以訪問外部的全域性變數,而外部無法訪問函式內部的區域性變數,這是JS的一個特點:

var n = 999;
function f1() {
    console.log(n) 
}
f1(); // 999 
複製程式碼

那麼擺在我們面前的一個問題就是:如何在函式外部訪問到函式內部的變數?有一種辦法就是在函式內部再定義一個函式,通過這個內層函式去訪問外層函式的變數,因為內層函式同屬外層函式的作用域中(符合鏈式作用域結構規則,子物件可以一級一級地向上尋找所有父物件的變數):

funtion f1() {
    var n = 999;
    function f2() {
        console.log(n);
    }
    return f2;
}

var result = f1();
result(); // 999
複製程式碼

對此:阮一峰老師給出了一個通俗的關於閉包的解釋: 閉包就是能夠讀取其他函式內部變數的函式(關於到底是內層函式是閉包還是外層函式是閉包的解釋各方不一致,但這不是重點)。

由於在JS中只有函式內部的子函式才能讀取區域性變數,因此也可以理解為定義在一個函式內部的函式。

二、讓變數始終儲存在記憶體中

暫且我們說閉包是指有權訪問另一個函式作用域中的變數的函式吧,函式內部的函式使用到外層函式的變數,使得外層函式的變數的生存時間延長,造成常駐記憶體。

function foo(){
    var a = 2;
    return function(){
      a += 1;
      console.log(a);
   }
}

var baz = foo();

baz(); // 3
baz(); // 4
baz(); // 5
baz(); // 6
複製程式碼

上面的例子為什麼每次呼叫baz時,a都沒有被初始化賦值呢? 接下來就要從JS的垃圾回收機制去考慮了。

1. 垃圾回收機制

通常我們可以使用引用計數法判斷程式碼中的變數是否被釋放,即語言引擎中有一張“引用表”,儲存了記憶體裡面所有的資源(通常是各種值)的引用次數,如果一個值的引用次數為0,則表示該值不再用到了,因此改值也被記憶體釋放了。比如:

cont arr = [1,2,3];
// 陣列[1,2,3]是一個值,會佔用記憶體變數arr是僅有的對這個陣列的引用,因此引用次數為1。儘管只賦值一次,後面程式碼中再沒有呼叫,但卻依然佔據記憶體.
複製程式碼

譬如JS這樣的高階程式語言中都嵌入了一種稱為垃圾回收器的機制,其工作是跟蹤記憶體的分配和使用,以便發現任何時候不再需要的記憶體並對其進行釋放。舉例如下:

var o1 = {
    o2: {
        x: 1
    }
};
//建立2個物件o2和o1,其中o2被o1物件引用作為其屬性,此時沒有垃圾可收集

var o3 = o1; //建立變數o3,引用由o1指向的物件的變數
o1 = 1 ; //現在將o1重新賦值為1,最初的o1中的物件由o3變數表示
var o4 = o3.o2; //建立變數o4,引用物件o2,此時o2被兩個地方引用:一個是作為o3變數的屬性,一個是作為o4變數
o3 = '666'; // 此時最初o1物件應沒有再被引用了,可以被垃圾收集了,但是最初的o2還在被o4引用,因此還不能被垃圾收集

o4 = 16; //此時 o2也可以說再見了...
複製程式碼

2. 解釋記憶體保留

好了,現在再回過頭來看看上面那個閉包題,用垃圾回收的思想來作出解答:

function foo(){
    var a = 2;
    function outer() {
        a += 1;
	console.log(a);
    }
    return outer
}

var baz = foo(); //在全域性作用域下建立變數baz

複製程式碼

此時,baz指向的就是 outer,所以outer始終都沒有被銷燬,而根據垃圾回收機制,由於在outer中有引用外層函式的變數a,因此a也一直沒有被銷燬,所以就出現這種現象:

baz(); //3
baz(); //4
baz(); //5
複製程式碼

是不是以下就明白了,接下來看一個簡單的經典例題

三、經典例題

看一下經典的面試題吧,用垃圾回收的思想來思想一下結果,就會很順利:

var callbacks;
for(var i = 0 ; i <= 5 ;i ++) {
   callbacks = function() {
      console.log(i);
   }
}
callbacks(); //6
callbacks(); //6
callbacks(); //6
複製程式碼

參考文獻

  1. 阮一峰老師 《學習Javascript閉包》 www.ruanyifeng.com/blog/2009/0…
  2. 《重讀JavaScript高階程式設計》 reng99.cc/2018/03/01/…

相關文章