欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳細(xì)聊聊瀏覽器是如何看閉包的

 更新時(shí)間:2021年11月10日 17:03:22   作者:lucefer  
閉包實(shí)質(zhì)上是函數(shù)作用域的副產(chǎn)物,下面這篇文章主要給大家介紹了關(guān)于瀏覽器是如何看閉包的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下

前言

閉包,是javascript的一大理解難點(diǎn),網(wǎng)上關(guān)于閉包的文章也很多,但是很少有能讓人看了就徹底明白的文章。究其原因,我想是因?yàn)殚]包涉及了一連串的知識(shí)點(diǎn)。只有把這一連串的知識(shí)點(diǎn)都理解透徹,實(shí)現(xiàn)一個(gè)概念的閉環(huán),才可以真正理解它。今天打算換個(gè)角度來(lái)理解閉包,從內(nèi)存分配與回收的角度闡述,希望能幫助你真正消化掉所看到的閉包知識(shí),同時(shí)也希望本文是你看的最后一篇關(guān)于閉包的文章。

大家看本文中的配圖時(shí),請(qǐng)牢記箭頭的指向。因?yàn)樗歉鶎?duì)象window遍歷內(nèi)存垃圾所依賴(lài)的原則,能夠從window開(kāi)始,順著箭頭找到的都不是內(nèi)存垃圾,不會(huì)被回收掉。只有那些找不到的對(duì)象才是內(nèi)存垃圾,才會(huì)在適當(dāng)?shù)臅r(shí)機(jī)被gc回收。

閉包簡(jiǎn)介

函數(shù)嵌套函數(shù)時(shí),內(nèi)層函數(shù)引用了外層函數(shù)作用域下的變量,并且內(nèi)層函數(shù)被全局環(huán)境下的變量引用,就形成了閉包。

閉包實(shí)質(zhì)上是函數(shù)作用域的副產(chǎn)物。

關(guān)于閉包我們需要特別重視的一點(diǎn)是函數(shù)內(nèi)部定義的所有函數(shù)共享同一個(gè)閉包對(duì)象。什么意思呢?看如下代碼:

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,同時(shí)f被外部變量a引用,所以形成閉包,導(dǎo)致變量d滯留在內(nèi)存中。我們思考一下,那么變量c呢?好像我們并沒(méi)有用到c,應(yīng)該不會(huì)滯留在內(nèi)存中吧。然后事實(shí)是c也會(huì)滯留在內(nèi)存中。如上代碼形成的閉包包含兩個(gè)成員,c和d。這種現(xiàn)象成為函數(shù)內(nèi)閉包共享。

為什么說(shuō)需要特別重視這個(gè)特性呢?因?yàn)檫@個(gè)特性,如果我們不仔細(xì)的話(huà),很容易寫(xiě)出導(dǎo)致內(nèi)存泄漏的代碼。
關(guān)于閉包的概念性的東西,我就講這么多了,但是如果真正理解好閉包,還是需要搞明白幾個(gè)知識(shí)點(diǎn)

  • 函數(shù)作用域鏈
  • 執(zhí)行上下文
  • 變量對(duì)象、活動(dòng)對(duì)象

這些內(nèi)容大家可以谷歌百度之,大概理解一下。接下來(lái)我會(huì)講如何從瀏覽器的視角來(lái)理解閉包,所以不做過(guò)多講解。

如何判別內(nèi)存垃圾

現(xiàn)代瀏覽器的垃圾回收過(guò)程比較復(fù)雜,詳細(xì)過(guò)程大家可以自行g(shù)oogle之。這里我只講如何判定內(nèi)存垃圾。大體上可以這么理解,從根對(duì)象開(kāi)始尋找,只要能順著引用找到的,都不能被回收。順著引用找不到的對(duì)象被視為垃圾,在下一個(gè)垃圾回收節(jié)點(diǎn)被回收。尋找垃圾,可以理解為順藤摸瓜的過(guò)程。

閉包的內(nèi)存表示

從最簡(jiǎn)單的代碼入手,我們看下全局變量定義。

var a = new String('小歌')

這樣一段代碼,在內(nèi)存里表示如下

在全局環(huán)境下,定義了一個(gè)變量a,并給a賦值了一個(gè)字符串,箭頭表示引用。

我們?cè)俣x一個(gè)函數(shù):

var a = new String('小歌')
function teach() {
  var b = new String('小谷')
}

內(nèi)存結(jié)構(gòu)如下:

一切都很好理解,如果你細(xì)心的話(huà),你會(huì)發(fā)現(xiàn)函數(shù)對(duì)象teach里有一個(gè)叫[[scopes]]的屬性,這是什么東東?函數(shù)創(chuàng)建完為什么會(huì)有這個(gè)屬性。很高興你能問(wèn)到這一點(diǎn),也是理解閉包很關(guān)鍵的一點(diǎn)。

請(qǐng)謹(jǐn)記: 函數(shù)一旦創(chuàng)建,javascript引擎會(huì)在函數(shù)對(duì)象上附加一個(gè)名叫作用域鏈的屬性,這個(gè)屬性指向一個(gè)數(shù)組對(duì)象,數(shù)組對(duì)象包含著函數(shù)的作用域以及父作用域,一直到全局作用域

所以上圖可以簡(jiǎn)單理解為:teach函數(shù)是在全局環(huán)境下創(chuàng)建的,所以teach的作用域鏈只有一層,那就是全局作用域global

需要明確的是,瀏覽器下global指向window對(duì)象,nodejs環(huán)境global指向global對(duì)象

請(qǐng)?jiān)俅沃?jǐn)記: 函數(shù)在執(zhí)行的時(shí)候,會(huì)申請(qǐng)空間創(chuàng)建執(zhí)行上下文,執(zhí)行上下文會(huì)包含函數(shù)定義時(shí)的作用域鏈,其次包含函數(shù)內(nèi)部定義的變量、參數(shù)等,當(dāng)函數(shù)在當(dāng)前作用域執(zhí)行時(shí),會(huì)首先查找當(dāng)前作用域下的變量,如果找不到,就會(huì)向函數(shù)定義時(shí)的作用域鏈中查找,直到全局作用域,如果變量在全局作用域下也找不到,則會(huì)拋出錯(cuò)誤。

我們都知道,函數(shù)執(zhí)行的時(shí)候,會(huì)創(chuàng)建一個(gè)執(zhí)行上下文,其實(shí)就是在申請(qǐng)一塊棧結(jié)構(gòu)的內(nèi)存空間,函數(shù)中的局部變量都在這塊空間中分配,函數(shù)執(zhí)行完畢,局部變量在下一個(gè)垃圾回收節(jié)點(diǎn)被回收。OK,我們?cè)俅紊?jí)一下代碼,看一下函數(shù)運(yùn)行時(shí)內(nèi)存的結(jié)構(gòu)。

var a = new String('小歌')
function teach() {
  var b = new String('小谷')
}
teach()

內(nèi)存表示如下:

很明顯,我們可以看到,函數(shù)在執(zhí)行過(guò)程中僅僅做了一個(gè)局部變量的賦值,并未與全局環(huán)境下的變量發(fā)生關(guān)系,所以我們從window對(duì)象沿著引用(圖中的箭頭)尋找的話(huà),是找不到執(zhí)行上下文中的變量b的。因此函數(shù)執(zhí)行完后,變量b將被回收。

我們?cè)俅紊?jí)一下代碼:

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

內(nèi)存表示如下:

注:灰色表示的是無(wú)法從根對(duì)象跟蹤到的對(duì)象。

函數(shù)執(zhí)行順序:

  1. 函數(shù)teach開(kāi)始執(zhí)行前,申請(qǐng)??臻g,上圖藍(lán)色方塊。
  2. 創(chuàng)建上下文scope(類(lèi)棧結(jié)構(gòu)),并將teach函數(shù)定義時(shí)的[[scopes]]壓入到scope中。
  3. 初始化變量b(變量提升),創(chuàng)建函數(shù)say,初始化say的scopes屬性,首先將函數(shù)teach的scopes壓入函數(shù)say的[[scopes]] 中。由于say引用了變量b,形成閉包c(diǎn)losure。所以我們還要將closure對(duì)象壓入函數(shù)say的[[scopes]]。
  4. 創(chuàng)建變量對(duì)象local,指向局部變量b和say,并將local壓入步驟2的scope中。
  5. 函數(shù)開(kāi)始執(zhí)行
    1. 給變量b賦值字符串對(duì)象'小谷'。
    2. 將全局變量a指向函數(shù)say。

函數(shù)執(zhí)行完畢,正常情況下變量b應(yīng)該被釋放了。但是我們發(fā)現(xiàn),沿著window找下去,是能夠找到b的,根據(jù)我們前面講的判定內(nèi)存垃圾的原理得知,b不是內(nèi)存垃圾,所以b不能被釋放,這就是為什么閉包會(huì)讓函數(shù)內(nèi)變量保存在內(nèi)存中的原因。

再次升級(jí)代碼,我們看下閉包共享的內(nèi)存表示:

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()

灰色表示的圖形是內(nèi)存垃圾,將會(huì)被垃圾回收器回收。

上圖很容易得出,雖然函數(shù)f沒(méi)有用到變量c,但是c被函數(shù)e引用,所以變量c存在于閉包c(diǎn)losure中,從window對(duì)象開(kāi)始尋找能夠找到變量c,所以變量c也不能釋放。

你也許會(huì)問(wèn)了,這種特性是如何能導(dǎo)致內(nèi)存泄漏的呢?好吧,思考如下一段代碼,比較經(jīng)典的meteor內(nèi)存泄漏問(wèn)題。

        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)

這段代碼是有內(nèi)存泄漏的,在瀏覽器中執(zhí)行這段代碼,你會(huì)發(fā)現(xiàn)內(nèi)存不斷上升,雖然gc釋放了一些內(nèi)存,但是仍然有一些內(nèi)存無(wú)法釋放,而且是梯度上升的。如下圖

這種曲線(xiàn)說(shuō)明是有內(nèi)存泄漏的,我們可以通過(guò)開(kāi)發(fā)者工具去分析哪些對(duì)象沒(méi)有被回收掉。事實(shí)上我可以告訴大家,沒(méi)有釋放掉的內(nèi)存其實(shí)就是我們每次創(chuàng)建的大對(duì)象t。我們通過(guò)畫(huà)圖的方式來(lái)看下:

上面這張圖是假設(shè)replaceThing函數(shù)執(zhí)行了三次,你會(huì)發(fā)現(xiàn),每次我們給變量t賦予一個(gè)大對(duì)象的時(shí)候,由于閉包共享的緣故,之前的大對(duì)象仍然能夠從window對(duì)象跟蹤到,所以這些大對(duì)象都不能被回收掉。其實(shí)真正對(duì)我們有用的是最后一次為t賦予的大對(duì)象,那么之前的對(duì)象則造成了內(nèi)存泄漏。

可以想象,假如我們沒(méi)有意識(shí)到這一點(diǎn),任由程序一直運(yùn)行下去,瀏覽器很快就會(huì)崩潰。

解決這個(gè)問(wèn)題的方式也很簡(jiǎn)單,每次執(zhí)行完代碼,將變量o置為null即可,大家可以試試看哈~

結(jié)語(yǔ)

到此這篇關(guān)于瀏覽器是如何看閉包的的文章就介紹到這了,更多相關(guān)瀏覽器如何看閉包內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論