閉包算是javascript中一個比較難理解的概念,想要深刻理解閉包的原理,首先須要搞清楚其餘幾個概念:javascript
1、棧內存和堆內存
學過C/C++的同窗可能知道,計算機系統將內存分爲棧和堆兩部分(大學的基礎課,忘掉的趕忙從新撿起來)。java
棧內存(連續的存儲空間,相似數據結構中的棧):主要用來存放數值、字符、內存地址等小數據數據結構
堆內存(散列的存儲空間,相似數據結構中的鏈表):存放能夠動態變化的大數據閉包
2、基本類型和引用類型
JavaScript將變量分爲兩種類型:函數
基本類型:Number、String、Boolean 、undefined、null(值被保存在棧內存中)大數據
引用類型:Object、Array、function(具體內容被保存在堆內存中,在棧內存中僅保存堆內存的地址)spa
如上圖,當在程序中在執行中有以下狀況:code
一、聲明變量a爲基本類型時,直接在棧內存中保存它的值爲100;對象
二、當將a賦值給b時,b在棧內存中新建空間,將a的值複製過來blog
(注:以後a和b就沒有關係了,再改變a或b的值,不影響另一個,它們是獨立的)
三、聲明變量p1爲引用類型時,將p1的內容保存在堆內存中,並將堆內存的物理地址保存在棧內存中
四、當將p1賦值給p2時,p2在棧內存中新建空間,僅複製堆內存的物理地址
(注:p1和p2中都保存的是指向堆內存的地址,即指的是同一個對象,當修改p1對象的屬性後,p2對象的屬性同時被修改)
另外,在計算機語言中還有一些很重要的特性:
一、修改基本類型的值,其實是新建空間存一個新值,而後將變量名指向新的空間(舊值依然存在棧內存中,只是缺乏變量名指向它)
二、刪除引用類型,其實並不刪除堆內存中的內容,僅刪除了棧內存中的物理地址(對象的內容依然存在堆內存中,只是缺乏了地址的指向)
(注:計算機關於內存的管理,跟咱們正常想到的不同,例如硬盤恢復就是利用這個原理,爲刪除的內容從新創建一個指向便可訪問)
2、變量做用域
javascript中變量又分爲全局變量和局部變量
全局變量:在全局環境中聲明的變量
局部變量:在函數中聲明的變量
當函數在執行時,會建立一個封閉的執行期上下文環境,函數內部聲明的變量僅可在函數內部使用,外部沒法訪問,而全局變量則在任何地方均可以使用
3、預編譯
JavaScript的運行爲三步:語法分析》預編譯》解釋執行
一、語法分析:通篇掃描js文件,檢查是否有低級語法錯誤
二、預編譯四部曲:(發生在解釋執行的前一刻)
a、建立AO對象(執行期上下文對象,全局爲GO)
b、將形參和變量聲明做爲AO對象的屬性名,值爲undefined
c、將實參值傳遞給形參,即賦值給AO對象對應屬性名
d、將函數聲明爲AO對象的方法名,值爲函數體
三、解釋執行:解釋一行,執行一行。
function test(a){ var b=1; function c(){} } test(2); /* 函數預編譯四部曲(函數執行前一刻,不執行不會預編譯),全局預編譯同理 * 1---testAO{} * 2---testAO{a:undefined,b:undefined} * 3---testAO{a:2,b:undefined} * 4---testAO{a:2,b:1,c:function(){}} */
4、做用域鏈
每一個JavaScript函數都是一個對象,對象中有些屬性能夠訪問(好比name),有些屬性不能夠訪問(好比[[scope]]僅供js引擎使用)
[[scope]]用來存儲了運行期上下文對象的集合(即做用域鏈),做用域鏈中除了自身建立的AO對象外,還包括了全部父級運行期上下文對象(AO)
function a(){ function b(){ var b = 234; } var a = 123; b(); } var glob = 100; a();
當b執行完成後,b的AO要被銷燬,即b的[[scope]]第0位將被置空,若是再次執行b,將新建一個新的AO將其地址存到第0位,
當a也執行完成後,a的AO要被銷燬,即a的[[scope]]第0位將被置空,同時a的AO中存着b,b也將被一同銷燬
在瞭解如上這些概念後,咱們再來看下面這個經典的閉包,你會有一個全新的認識
function a(){ var b=123; function c(){ console.log(b+=1); } return c; } var d=a(); d();
當這段代碼在執行時的順序以下:
一、預編譯全局,生成執行上下文對象GO{d:undefined,a:function(){}}
二、定義a函數,將a函數的[[scope]]屬性設置爲{0:GO}
三、預編譯a函數,生成a的執行上下文對象aAO{b:undefined,c:function(){}},修改a函數的[[scope]]屬性爲{0:aAO,1:GO}
四、執行a函數,給aAO的屬性賦值{b:123,c:function(){}}
五、定義c函數,將c函數的[[scope]]屬性設置爲{0:aAO,1:GO},並將c返回給d
六、a函數執行完畢,銷燬[[scope]]屬性第0位對aAO對象的引用
七、執行d函數(等於執行c函數)以前,先預編譯生成c的執行上下文對象cAO{},修改c函數的[[scope]]屬性爲{0:cAO,1:aAO,2:GO}
八、執行c函數,b變量在cAO中沒有,到[[scope]]屬性中的下一位aAO中獲取
個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan