本文是《后端程序員的 JavaScript 之旅 - 作用域鏈、執(zhí)行上下文與閉包》的學習筆記,有興趣的朋友可以去閱讀以下原文。
JavaScript 采用詞法作用域(lexical scoping)
先來解釋這句話,JavaScript 采用詞法作用域(lexical scoping),函數執(zhí)行依賴的變量作用域是由函數定義的時候決定,而不是函數執(zhí)行的時候決定。猜猜下面代碼的運行結果
var scope = 'global scope';
function foo() {
var scope = 'local scope';
return function () {
return scope;
}
}
var f = foo();
f()// 返回 "local scope"
用多了腳本語言,搞得我已經忘了,按照傳統(tǒng)的格式嚴密的C語言,Java語言的思維邏輯應該如何解讀這段代碼。好吧,以C語言的嚴謹邏輯理解好像是這樣的吧,return function(){}返回的是一個函數指針,然后在外面用f()調用了這個函數,然后函數查找當前函數作用上下文有沒有scope,沒有發(fā)現scope,所以返回了全局scope變量的值‘global scope’。好像是這樣的吧,我也搞不太清楚了(歡迎探討)。可是這里JavaScript返回的卻是local scope。這里面涉及了一個”高大上“的概念——閉包。函數執(zhí)行依賴的變量作用域是由函數定義的時候決定,而不是函數執(zhí)行的時候決定。這句話說實話有點難懂或者說抽象。我想通俗且具象的來解釋一下JavaScript閉包的實現機制。
首先,在JavaScript中一切皆對象,函數也是對象,每個函數都有自己的屬性。你可以這樣考慮,當你寫這樣一段代碼function foo(){}的時候其實是創(chuàng)建了一個對象而并不是定義了一個函數。有了這個概念很多東西就很好理解了。那么當你創(chuàng)建了一個函數對象,這個函數對象里都有什么呢?我先講與閉包相關的屬性,沒錯它就叫<Closure>屬性,是一個有序列表,列表中裝的就是“作用域”,它們的排列順序是當前函數作用域——上層函數作用域——全局作用域。所以說當函數想訪問一個變量的時候它先查找當前作用域,然后查找上層函數作用域直到全局作用域。而這一切都在在這個函數的自身的<Closure>屬性中查找,而這些屬性在“函數定義”的時候(實際上是函數對象的創(chuàng)建)就已經設置到函數對象里了(它在函數對象里,卻不能用JS代碼來訪問,這個屬于只有瀏覽器才能操作的屬性),所以說函數執(zhí)行依賴的變量作用域是由函數定義的時候決定的。
我這里插播一段對函數對象籠統(tǒng)解釋。函數對象比普通對象多了 prototype和<Closure>屬性,用構造函數生成的普通對象的_proto_指向它的構造函數的prototype屬性,prototype屬性里默認包含了constructor屬性(即函數本身)和_proto_(Object類型)。<Closure>屬性是一個列表,里面的元素指向了當前函數所有上層函數的作用空間,變量查詢的時候由近及遠,直到最上層的Window是對象(也就是全局作用與)。
現在重新解釋一下上面的代碼
//定義全局變量,屬于Window作用域
var scope = 'global scope';
//創(chuàng)建一個叫foo的函數對象它<Closure>屬性里面有一個foo作用域
function foo() {
//在foo作用域里創(chuàng)建scope變量
var scope = 'local scope';
//創(chuàng)建一個匿名函數對象,它的<Closure>屬性里除了有自己的作用域,還有foo和Window的作用域
return function () {
return scope;
}
}
//調用foo函數foo函數返回它內部創(chuàng)建的匿名函數對象
var f = foo();
//執(zhí)行匿名函數,獲得scope變量,這個函數自己的作用域里沒有scope,它就找上層函數對象foo的作用域,發(fā)現里面有一個scope變量,于是返回
f()
思考一下下面的代碼,想一想如何改寫最里面的匿名函數,讓程序可以訪問到"my scope"
var scope = 'global scope';
function foo() {
var scope = 'local scope';
function innerFun(){
var scope = 'inner scope'
return function(){
this.getScope = function (){
return this.scope;
}
return scope;
};
}
return new innerFun();
}
// var ff = new foo();
var f = foo();
console.log(f()); // 返回 "local scope"
f.scope = "my scope";
console.log(f());
console.log(getScope())
輸出結果
inner scope
inner scope
global scope
不得不說,用JavaScript寫的代碼給人一種謎一般的感覺。我基本已經被繞暈了。不過用上面我寫的概念分析這段代碼還是沒問題的。這里有一個新的關鍵字new我決定再開一篇文章總結。沒想到光寫一個詞法作用域就寫了這么多。《后端程序員的 JavaScript 之旅 - 作用域鏈、執(zhí)行上下文與閉包》中提到的其他概念我再開一片文章分析吧。歡迎大家熱烈討論
對不住大家了,我這篇文章上面的解釋可能會導致一些誤解,在此更正一下。當一個函數配定義的時候,在這個函數內部被聲明的局部變量并沒有被寫入<Closure>屬性,函數被執(zhí)行的時候局部變量才會被創(chuàng)建,被寫入<Closure>的只有定義當前函數的上層函數的局部變量,一直到全局變量(更準確的說應該是變量的引用)。也就是說上面的代碼我的注釋是有錯誤的。我重新解釋一下,對比我之前注釋,大家可以加深對這個問題的理解。
//定義全局變量,屬于Window作用域
var scope = 'global scope';
//創(chuàng)建一個叫foo的函數對象它<Closure>屬性里面沒有任何屬性
function foo() {
//在foo作用域里創(chuàng)建scope變量
var scope = 'local scope';
//創(chuàng)建一個匿名函數對象,它的<Closure>屬性有foo作用域中的局部變量scope的引用
return function () {
return scope;
}
}
//調用foo函數foo函數返回它內部創(chuàng)建的匿名函數對象
var f = foo();
//執(zhí)行匿名函數,這個函數自己并沒有定義scope變量,它就到<Closure>里找上層函數對象foo的作用域,發(fā)現里面有一個scope變量,于是返回
f()
所以說函數內部局部變量是在函數執(zhí)行的時候被創(chuàng)建的,而函數對象閉包屬性中的變量是在函數定義時被它的上層函數創(chuàng)建,并賦給這個函數對象的<Closure>屬性。
這里面又引出了另一個問題,沒有閉包概念的編程語言,函數內部定義局部變量后,它們會被存儲到一個函數棧中,當函數調用結束,這些局部變量會出棧,永久銷毀,無法用任何方式訪問。簡單來說JavaScript的函數局部變量也差不多,當函數調用結束,這些局部變量會被標注成垃圾等待垃圾回收,無法被訪問到。但是有了閉包這個概念就不一樣了,如果在一個函數的內部定義另一個函數,它會把自己的局部變量的引用傳遞給它定義的函數對象,當這個子函數對象被返回給外部的時候,即使當前函數執(zhí)行完畢,它的局部變量的引用仍然在“活在”某一個函數對象里,所以這些的局部變量不會被垃圾回收,這就是閉包的核心概念,既函數可以訪問它上層定義它的函數的局部變量。由此我們可知,閉包的濫用,會導致應該回收的內存(函數作用域范圍內的局部變量)得不到回收,從而會導致內存泄露。當然,現在的的計算機內存如此之大,想寫出一個能把瀏覽器內存都吃沒的程序也不是那么容易的,所以日常編程中也沒有哪個前端程序員會考慮內存泄露這個問題。