我的JavaScript之旅——“閉包”是什么時候創建的
直接看代碼:
var x = 1;
function Inner(y) {return x + y};
return Inner;
}
對于這樣一個簡單的閉包函數,下面兩種調用方式有什么不一樣的地方?
var inner1 = Outer();
var result = inner1(2); //3
var result = Outer()(2); //3
此篇試圖解答這個問題。先復習一下:
上篇文章說到,每次執行一個function時,就會進入一個新的“執行上下文”(execution context)。context的幾個重要屬性:
1, 有一個對應的variable object;在global context中就是global object。
2, 有一個對應的scope chain,這個scope chain的第一個object就是variable object;
3, 有一個不變的this變量。
其中第2條,scope chain是JS實現閉包(closure)的關鍵所在,這篇對“閉包”展開描述,以加深印象。后續文章將對this專門探討。
Variable object的實例化三部曲
我們已經知道,當JS執行時碰到一個變量,它會到scope chain里遞歸去找,而scope chain是由variable object和global object組成的一個object chain,global object包含JS預定義好的所有object,function的variable object則會包含函數內聲明的所有東西,包括function的參數、內部函數、局部變量。創建Variable object的過程有三步,上篇有寫過,這邊是官方的文檔。簡述如下:
1, 為variable object創建與函數參數同名的屬性;屬性值為傳入的參數值。
2, 對于“function declaration(見下節)”,首先創建函數,然后為variable object創建屬性;屬性值即為該函數實例。覆蓋1的同名屬性。
3, 為variable object創建各變量的屬性;屬性值為undefined。不覆蓋1,2的同名屬性。
function declaration vs. function statement
下面就是一個function declaration:
}
下面是一個function statement:
}
這是創建函數的三種方式之二,區別在上面的三部曲中就可以看出來:
1, 在進入execution context時(還沒執行任何代碼),function declaration就已經在第2步創建函數實例起來了;而function statement屬于第3步,而且初始值是undefined,要到執行這行代碼時,函數才會被創建起來。
2, function statement不會覆蓋同名的function declaration。
驗證一下:
如圖:A可以先用再聲明;B則不行。B在執行到最后一句之前,都是undefined。但是B這個屬性是一開始就在的:
(this是什么?因為上面的代碼是執行在global context中,B屬性應該創建到global object身上,也就是this。下篇會詳述this。)
三部曲中說了,function declaration會覆蓋同名的1里的參數,所以它是varaible object里的第一等公民:
而function statement不會。
閉包的創建時機
對于開頭的這段代碼:
var x = 1;
function Inner(y) {return x + y};
return Inner;
}
然后執行 :
回憶一下這時會發生什么?
會進入一個新的“執行上下文”(Outer Context),創建OuterVariableObject(有x和Inner屬性),放到OuterScopeChain的最前方。而且會創建Inner函數,創建時把當前Scope Chain作為Inner函數的[[Scope]]屬性。這是重點。
創建起來的Inner函數被inner1變量引用,Inner有[[Scope]]屬性,引用了OuterScopeChain,即[OuterVariableObject, global object],而OuterVariableObject又引用了局部變量x。所以inner1變量就對Outer函數體內的局部變量x有間接的引用。內部函數對外部函數的變量有了引用關系——閉包就是這時產生的。每次對外部函數的調用,都會產生一次閉包。
garbage collection
很多人都聽過閉包容易引起內存泄露。為什么呢?因為如上所述,inner1變量對x有間接引用,而inner1是聲明在global context下的一個變量,它在global context下隨時可以被用,那么JS的垃圾回收器就不會回收它(inner1),當然也就不會回收它所引用的x——直到退出global context,也就是我們關掉網頁的時候。
這就是文章開頭兩種調用方式的區別:方式1,x在關掉網頁前一直不能被回收;而方式2,x會被回收。
后續文章將介紹閉包的用處,和this關鍵字(比我想象的復雜)。