瀏覽器是怎麼看閉包的。

lucefer發表於2017-07-27

文章備份地址點這裡

閉包,是javascript的一大理解難點,網上關於閉包的文章也很多,但是很少有能讓人看了就徹底明白的文章。究其原因,我想是因為閉包涉及了一連串的知識點。只有把這一連串的知識點都理解透徹,實現一個概念的閉環,才可以真正理解它。今天打算換個角度來理解閉包,從記憶體分配與回收的角度闡述,希望能幫助你真正消化掉所看到的閉包知識,同時也希望本文是你看的最後一篇關於閉包的文章。

大家看本文中的配圖時,請牢記箭頭的指向。因為它是根物件window遍歷記憶體垃圾所依賴的原則,能夠從window開始,順著箭頭找到的都不是記憶體垃圾,不會被回收掉。只有那些找不到的物件才是記憶體垃圾,才會在適當的時機被gc回收。

閉包簡介

函式巢狀函式時,內層函式引用了外層函式作用域下的變數,並且內層函式被全域性環境下的變數引用,就形成了閉包。

閉包實質上是函式作用域的副產物。

關於閉包我們需要特別重視的一點是函式內部定義的所有函式共享同一個閉包物件
什麼意思呢?看如下程式碼:

var a
function b() {
  var c = new String('1')
  var d = new String('2')
  function e() {
    console.log(c)
  }
  function f() {
    console.log(d)
  }
  return f
}
a = b()複製程式碼

上面程式碼中f引用了變數d,同時f被外部變數a引用,所以形成閉包,導致變數d滯留在記憶體中。我們思考一下,那麼變數c呢?好像我們並沒有用到c,應該不會滯留在記憶體中吧。然後事實是c也會滯留在記憶體中。如上程式碼形成的閉包包含兩個成員,c和d。這種現象成為函式內閉包共享。

為什麼說需要特別重視這個特性呢?因為這個特性,如果我們不仔細的話,很容易寫出導致記憶體洩漏的程式碼。

關於閉包的概念性的東西,我就講這麼多了,但是如果真正理解好閉包,還是需要搞明白幾個知識點

  • 函式作用域鏈
  • 執行上下文
  • 變數物件、活動物件

這些內容大家可以谷歌百度之,大概理解一下。接下來我會講如何從瀏覽器的視角來理解閉包,所以不做過多講解。

如何判別記憶體垃圾

現代瀏覽器的垃圾回收過程比較複雜,詳細過程大家可以自行google之。這裡我只講如何判定記憶體垃圾。大體上可以這麼理解,從根物件開始尋找,只要能順著引用找到的,都不能被回收。順著引用找不到的物件被視為垃圾,在下一個垃圾回收節點被回收。尋找垃圾,可以理解為順藤摸瓜的過程。

閉包的記憶體表示

從最簡單的程式碼入手,我們看下全域性變數定義。

var a = new String('小歌')複製程式碼

這樣一段程式碼,在記憶體裡表示如下

在全域性環境下,定義了一個變數a,並給a賦值了一個字串,箭頭表示引用。

我們再定義一個函式:

var a = new String('小歌')
function teach() {
  var b = new String('小谷')
}複製程式碼

記憶體結構如下:

一切都很好理解,如果你細心的話,你會發現函式物件teach裡有一個叫[[scopes]]的屬性,這是什麼東東?函式建立完為什麼會有這個屬性。很高興你能問到這一點,也是理解閉包很關鍵的一點。

請謹記:
函式一旦建立,javascript引擎會在函式物件上附加一個名叫作用域鏈的屬性,這個屬性指向一個陣列物件,陣列物件包含著函式的作用域以及父作用域,一直到全域性作用域

所以上圖可以簡單理解為:teach函式是在全域性環境下建立的,所以teach的作用域鏈只有一層,那就是全域性作用域global

需要明確的是,瀏覽器下global指向window物件,nodejs環境global指向global物件

請再次謹記:
函式在執行的時候,會申請空間建立執行上下文,執行上下文會包含函式定義時的作用域鏈,其次包含函式內部定義的變數、引數等,當函式在當前作用域執行時,會首先查詢當前作用域下的變數,如果找不到,就會向函式定義時的作用域鏈中查詢,直到全域性作用域,如果變數在全域性作用域下也找不到,則會丟擲錯誤。

我們都知道,函式執行的時候,會建立一個執行上下文,其實就是在申請一塊棧結構的記憶體空間,函式中的區域性變數都在這塊空間中分配,函式執行完畢,區域性變數在下一個垃圾回收節點被回收。OK,我們再次升級一下程式碼,看一下函式執行時記憶體的結構。

var a = new String('小歌')
function teach() {
  var b = new String('小谷')
}
teach()複製程式碼

記憶體表示如下:

很明顯,我們可以看到,函式在執行過程中僅僅做了一個區域性變數的賦值,並未與全域性環境下的變數發生關係,所以我們從window物件沿著引用(圖中的箭頭)尋找的話,是找不到執行上下文中的變數b的。因此函式執行完後,變數b將被回收。

我們再次升級一下程式碼:

var a = new String('小歌')
function teach() {
  var b = new String('小谷')
  var say = function() {
    console.log(b)
  }
  a =  say
}
teach()複製程式碼

記憶體表示如下:

注:灰色表示的是無法從根物件跟蹤到的物件。

函式執行順序:

  1. 函式teach開始執行前,申請棧空間,上圖藍色方塊。
  2. 建立上下文scope(類棧結構),並將teach函式定義時的[[scopes]]壓入到scope中。
  3. 初始化變數b(變數提升),建立函式say,初始化say的scopes屬性,首先將函式teach的scopes壓入函式say的[[scopes]] 中。由於say引用了變數b,形成閉包closure。所以我們還要將closure物件壓入函式say的[[scopes]]。
  4. 建立變數物件local,指向區域性變數b和say,並將local壓入步驟2的scope中。
  5. 函式開始執行
    1. 給變數b賦值字串物件'小谷'。
    2. 將全域性變數a指向函式say。

函式執行完畢,正常情況下變數b應該被釋放了。但是我們發現,沿著window找下去,是能夠找到b的,根據我們前面講的判定記憶體垃圾的原理得知,b不是記憶體垃圾,所以b不能被釋放,這就是為什麼閉包會讓函式內變數儲存在記憶體中的原因。

再次升級程式碼,我們看下閉包共享的記憶體表示:

var a = new String('0')
function b() {
  var c = new String('1')
  var d = new String('2')
  function e() {
    console.log(c)
  }
  function f() {
    console.log(d)
  }
  return f
}
a = b()複製程式碼

灰色表示的圖形是記憶體垃圾,將會被垃圾回收器回收。

上圖很容易得出,雖然函式f沒有用到變數c,但是c被函式e引用,所以變數c存在於閉包closure中,從window物件開始尋找能夠找到變數c,所以變數c也不能釋放。

你也許會問了,這種特性是如何能導致記憶體洩漏的呢?好吧,思考如下一段程式碼,比較經典的meteor記憶體洩漏問題。

        var t = null;
        var replaceThing = function() {
            var o = t
            var unused = function() {
                if (o)
                    console.log("hi")
            }
            t = {
                    longStr: new Array(1000000).join('*'),
                    someMethod: function() {
                      console.log(1)
                    }
                }
        }
        setInterval(replaceThing, 1000)複製程式碼

這段程式碼是有記憶體洩漏的,在瀏覽器中執行這段程式碼,你會發現記憶體不斷上升,雖然gc釋放了一些記憶體,但是仍然有一些記憶體無法釋放,而且是梯度上升的。如下圖

這種曲線說明是有記憶體洩漏的,我們可以通過開發者工具去分析哪些物件沒有被回收掉。事實上我可以告訴大家,沒有釋放掉的記憶體其實就是我們每次建立的大物件t。我們通過畫圖的方式來看下:

上面這張圖是假設replaceThing函式執行了三次,你會發現,每次我們給變數t賦予一個大物件的時候,由於閉包共享的緣故,之前的大物件仍然能夠從window物件跟蹤到,所以這些大物件都不能被回收掉。其實真正對我們有用的是最後一次為t賦予的大物件,那麼之前的物件則造成了記憶體洩漏。

可以想象,假如我們沒有意識到這一點,任由程式一直執行下去,瀏覽器很快就會崩潰。

解決這個問題的方式也很簡單,每次執行完程式碼,將變數o置為null即可,大家可以試試看哈~

結語

文章到此結束,建議大家看一下自己曾經遇到的閉包例子,採用畫圖的方式,我想你會很容易的理解它。如果沒有,歡迎和我私下溝通。

相關文章