閉包算是javascript中一個比較難理解的概念,想要深入理解閉包的原理,首先需要搞清楚其他幾個概念: 一、棧記憶體和堆記憶體 學過C/C++的同學可能知道,電腦系統將記憶體分為棧和堆兩部分(大學的基礎課,忘掉的趕緊重新撿起來)。 棧記憶體(連續的存儲空間,類似數據結構中的棧):主要用來存放數值、字元、 ...
閉包算是javascript中一個比較難理解的概念,想要深入理解閉包的原理,首先需要搞清楚其他幾個概念:
一、棧記憶體和堆記憶體
學過C/C++的同學可能知道,電腦系統將記憶體分為棧和堆兩部分(大學的基礎課,忘掉的趕緊重新撿起來)。
棧記憶體(連續的存儲空間,類似數據結構中的棧):主要用來存放數值、字元、記憶體地址等小數據
堆記憶體(散列的存儲空間,類似數據結構中的鏈表):存放可以動態變化的大數據
二、基本類型和引用類型
JavaScript將變數分為兩種類型:
基本類型:Number、String、Boolean 、undefined、null(值被保存在棧記憶體中)
引用類型:Object、Array、function(具體內容被保存在堆記憶體中,在棧記憶體中僅保存堆記憶體的地址)
如上圖,當在程式中在執行中有如下情況:
1、聲明變數a為基本類型時,直接在棧記憶體中保存它的值為100;
2、當將a賦值給b時,b在棧記憶體中新建空間,將a的值複製過來
(註:之後a和b就沒有關係了,再改變a或b的值,不影響另外一個,它們是獨立的)
3、聲明變數p1為引用類型時,將p1的內容保存在堆記憶體中,並將堆記憶體的物理地址保存在棧記憶體中
4、當將p1賦值給p2時,p2在棧記憶體中新建空間,僅複製堆記憶體的物理地址
(註:p1和p2中都保存的是指向堆記憶體的地址,即指的是同一個對象,當修改p1對象的屬性後,p2對象的屬性同時被修改)
另外,在電腦語言中還有一些很重要的特性:
1、修改基本類型的值,實際上是新建空間存一個新值,然後將變數名指向新的空間(舊值依然存在棧記憶體中,只是缺少變數名指向它)
2、刪除引用類型,其實並不刪除堆記憶體中的內容,僅刪除了棧記憶體中的物理地址(對象的內容依然存在堆記憶體中,只是缺少了地址的指向)
(註:電腦關於記憶體的管理,跟我們正常想到的不一樣,例如硬碟恢復就是利用這個原理,為刪除的內容重新建立一個指向即可訪問)
二、變數作用域
javascript中變數又分為全局變數和局部變數
全局變數:在全局環境中聲明的變數
局部變數:在函數中聲明的變數
當函數在執行時,會創建一個封閉的執行期上下文環境,函數內部聲明的變數僅可在函數內部使用,外部無法訪問,而全局變數則在任何地方都可以使用
三、預編譯
JavaScript的運行為三步:語法分析》預編譯》解釋執行
1、語法分析:通篇掃描js文件,檢查是否有低級語法錯誤
2、預編譯四部曲:(發生在解釋執行的前一刻)
a、創建AO對象(執行期上下文對象,全局為GO)
b、將形參和變數聲明作為AO對象的屬性名,值為undefined
c、將實參值傳遞給形參,即賦值給AO對象對應屬性名
d、將函數聲明為AO對象的方法名,值為函數體
3、解釋執行:解釋一行,執行一行。
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(){}} */
四、作用域鏈
每個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();
當這段代碼在執行時的順序如下:
1、預編譯全局,生成執行上下文對象GO{d:undefined,a:function(){}}
2、定義a函數,將a函數的[[scope]]屬性設置為{0:GO}
3、預編譯a函數,生成a的執行上下文對象aAO{b:undefined,c:function(){}},修改a函數的[[scope]]屬性為{0:aAO,1:GO}
4、執行a函數,給aAO的屬性賦值{b:123,c:function(){}}
5、定義c函數,將c函數的[[scope]]屬性設置為{0:aAO,1:GO},並將c返回給d
6、a函數執行完畢,銷毀[[scope]]屬性第0位對aAO對象的引用
7、執行d函數(等於執行c函數)之前,先預編譯生成c的執行上下文對象cAO{},修改c函數的[[scope]]屬性為{0:cAO,1:aAO,2:GO}
8、執行c函數,b變數在cAO中沒有,到[[scope]]屬性中的下一位aAO中獲取
我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan