瀏覽器是怎麼看閉包的。

文章備份地址點這裏javascript

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

你們看本文中的配圖時,請牢記箭頭的指向。由於它是根對象window遍歷內存垃圾所依賴的原則,可以從window開始,順着箭頭找到的都不是內存垃圾,不會被回收掉。只有那些找不到的對象纔是內存垃圾,纔會在適當的時機被gc回收。node

閉包簡介

函數嵌套函數時,內層函數引用了外層函數做用域下的變量,而且內層函數被全局環境下的變量引用,就造成了閉包。數組

閉包實質上是函數做用域的副產物。瀏覽器

關於閉包咱們須要特別重視的一點是函數內部定義的全部函數共享同一個閉包對象
什麼意思呢?看以下代碼:閉包

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。這種現象成爲函數內閉包共享。函數

爲何說須要特別重視這個特性呢?由於這個特性,若是咱們不仔細的話,很容易寫出致使內存泄漏的代碼。工具

關於閉包的概念性的東西,我就講這麼多了,可是若是真正理解好閉包,仍是須要搞明白幾個知識點ui

  • 函數做用域鏈
  • 執行上下文
  • 變量對象、活動對象

這些內容你們能夠谷歌百度之,大概理解一下。接下來我會講如何從瀏覽器的視角來理解閉包,因此不作過多講解。google

如何判別內存垃圾

現代瀏覽器的垃圾回收過程比較複雜,詳細過程你們能夠自行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便可,你們能夠試試看哈~

結語

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

相關文章
相關標籤/搜索