您好,登錄后才能下訂單哦!
小編給大家分享一下javascript中瀏覽器是如何看閉包的,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
閉包,是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()
內存表示如下:
注:灰色表示的是無法從根對象跟蹤到的對象。
函數執行順序:
函數teach開始執行前,申請棧空間,上圖藍色方塊。
創建上下文scope(類棧結構),并將teach函數定義時的[[scopes]]壓入到scope中。
初始化變量b(變量提升),創建函數say,初始化say的scopes屬性,首先將函數teach的scopes壓入函數say的[[scopes]] 中。由于say引用了變量b,形成閉包closure。所以我們還要將closure對象壓入函數say的[[scopes]]。
創建變量對象local,指向局部變量b和say,并將local壓入步驟2的scope中。
函數開始執行
給變量b賦值字符串對象'小谷'。
將全局變量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即可。
以上是“javascript中瀏覽器是如何看閉包的”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。