來源于 現(xiàn)代JavaScript教程
閉包章節(jié)
中文翻譯計劃
本文很清晰地解釋了閉包是什么,以及閉包如何產(chǎn)生,相信你看完也會有所收獲
關(guān)鍵字
Closure 閉包
Lexical Environment 詞法環(huán)境
Environment Record 環(huán)境記錄
閉包(Closure)
JavaScript 是一個 function-oriented 的語言。這帶來了很大的操作自由。函數(shù)只需創(chuàng)建一次,可以拷貝到另一個變量,或者作為一個參數(shù)傳入另一個函數(shù)然后在一個全新的環(huán)境調(diào)用。
我們知道函數(shù)可以訪問它外部的變量,這個 feature 十分常用。
但是當外部變量改變時會發(fā)生什么?函數(shù)時獲取最新的值,還是函數(shù)創(chuàng)建當時的值?
還有一個問題,當函數(shù)被送到其他地方再調(diào)用……他能訪問那個地方的外部變量嗎?
不同語言的表現(xiàn)有所不同,下面我們研究一下 JavaScript 中的表現(xiàn)。
兩個問題
我們先思考下面兩種情況,看完這篇文章你就可以回答這兩個問題,更復雜的問題也不在話下。
-
sayHi函數(shù)使用了外部變量name。函數(shù)運行時,會使用兩個值中的哪個?let name = "John"; function sayHi() { alert("Hi, " + name); } name = "Pete"; sayHi(); // "John" 還是 "Pete"?這個情況不論是瀏覽器端還是服務器端都很常見。函數(shù)很可能在它創(chuàng)建一段時間后才執(zhí)行,例如等待用戶操作或者網(wǎng)絡請求。
問題是:函數(shù)是否會選擇變量最新的值呢?
-
makeWorker函數(shù)創(chuàng)造并返回了另一個函數(shù)。這個新函數(shù)可以在任何地方調(diào)用。他會訪問創(chuàng)建時的變量還是調(diào)用時的變量呢?function makeWorker() { let name = "Pete"; return function() { alert(name); }; } let name = "John"; // 創(chuàng)建函數(shù) let work = makeWorker(); // 調(diào)用函數(shù) work(); // "Pete" (創(chuàng)建時) 還是 "John" (調(diào)用時)?
Lexical Environment (詞法環(huán)境)
要理解里面發(fā)生了什么,必須先明白“變量”到底是什么。
在 JavaScript 里,任何運行的函數(shù)、代碼塊、整個 script 都會關(guān)聯(lián)一個被叫做 Lexical Environment (詞法環(huán)境) 的對象。
Lexical Environment 對象包含兩個部分:(譯者:這里是重點)
-
Environment Record (環(huán)境記錄)是一個擁有全部局部變量作為屬性的對象(以及其他如
this值的信息)。 - outer lexical environment (外部詞法環(huán)境)的引用,通常詞法關(guān)聯(lián)外面一層代碼(花括號外一層)。
所以,“變量”就是內(nèi)部對象 Environment Record 的一個屬性。要改變一個對象,意味著改變 Lexical Environment 的屬性。
例如在這段簡單的代碼中,只有一個 Lexical Environment:

這就是所謂 global Lexical Environment (全局語法環(huán)境),對應整個 script。對于瀏覽端,整個 <script> 標簽共享一個全局環(huán)境。
(譯者:這里是重點)
上圖中,正方形代表 Environment Record (變量儲存),箭頭代表 outer reference (外部引用)。global Lexical Environment 沒有外部引用,所以指向 null。
下圖展示 let 變量的工作機制:

右邊的正方形描述 global Lexical Environment 在執(zhí)行中如何改變:
- 腳本開始運行,Lexical Environment 空。
-
let phrase定義出現(xiàn)了。因為沒有賦值所以儲存為undefined。 -
phrase被賦值。 -
phrase被賦新值。
看起來很簡單對不對?
總結(jié):
- 變量是一個特殊內(nèi)部對象的屬性,關(guān)聯(lián)于執(zhí)行時的塊、函數(shù)、 script 。
- 對變量的操作實際上是對這個對象屬性的操作。
Function Declaration (函數(shù)聲明)
Function Declaration 并非處理于被執(zhí)行的時候,而是 Lexical Environment 創(chuàng)建的時候。對于 global Lexical Environment ,這意味著 script 開始運行的時候。
這就是函數(shù)可以在定義前調(diào)用的原因。
以下代碼 Lexical Environment 開始時非空。因為有 say 函數(shù)聲明,之后又有了 let 聲明的 phrase :

Inner and outer Lexical Environment (內(nèi)部詞法環(huán)境和外部詞法環(huán)境)
調(diào)用 say() 的過程中,它使用了外部變量,一起看看這里面發(fā)生了什么。
(譯者:這里是重點)
函數(shù)運行時會自動創(chuàng)建一個新的函數(shù) Lexical Environment 。這是所有函數(shù)的通用規(guī)則。這個新的 Lexical Environment 用于當前運行函數(shù)的存放局部變量和形參。
箭頭標記的是執(zhí)行 say("John") 時的 Lexical Environment :

函數(shù)調(diào)用過程中,可以看到兩個 Lexical Environment :里面的是函數(shù)調(diào)用產(chǎn)生的,外面的是全局的:
- 內(nèi)層 Lexical Environment 對應當前執(zhí)行的
say。它只有一個變量: 函數(shù)實參name。我們調(diào)用say("John"),所以name的值是"John"。 - 外層 Lexical Environment 是 global Lexical Environment 。
內(nèi)層 Lexical Environment 有一個 outer 屬性,指向外層 Lexical Environment。
代碼要訪問一個變量,首先搜索內(nèi)層 Lexical Environment ,接著是外層,再外層,直到鏈的結(jié)束。
如果走完整條鏈變量都找不到,在 strict mode 就會報錯了。不使用 use strict 的情況下,對未定義變量的賦值,會創(chuàng)造一個新的全局變量。
下面一起看看變量搜索如何處理:
-
say里的alert想要訪問name,立即就能在當前函數(shù)的 Lexical Environment 找到。 - 對于
phrase,局部變量不存在phrase,所以要循著outer在全局變量里找到。

現(xiàn)在我們可以回答本章開頭的第一個問題了。
函數(shù)獲取外部變量當前值
舊變量值不儲存在任何地方,函數(shù)需要他們的時候,它取得來源于自身或外部 Lexical Environment 的當前值。
所以第一個問題的答案是 Pete :
let name = "John";
function sayHi() {
alert("Hi, " + name);
}
name = "Pete"; // (*)
sayHi(); // Pete
上述代碼的執(zhí)行流:
- global Lexical Environment 存在
name: "John"。 -
(*)行中,全局變量修改了,現(xiàn)在成了這樣name: "Pete"。 -
say()執(zhí)行的時候, 取外部name。此時在 global Lexical Environment 中已經(jīng)是"Pete"。
一次調(diào)用,一個 Lexical Environment
請注意,每當一個函數(shù)運行,就會創(chuàng)建一個新的 function Lexical Environment。
如果一個函數(shù)被多次調(diào)用,那么每次調(diào)用都會生成一個屬于當前調(diào)用的全新 Lexical Environment ,里面裝載著當前調(diào)用的變量和實參。
Lexical Environment 是一個標準對象 (specification object)
"Lexical Environment" 是一個標準對象 (specification object)。我們不能直接獲取或設置它,JavaScript 引擎也可能優(yōu)化它,拋棄未使用的變量來節(jié)省內(nèi)存或者作其他優(yōu)化,但是可見行為應該如上面所述。
嵌套函數(shù)
在一個函數(shù)中創(chuàng)建另一個函數(shù),稱為“嵌套”。這在 JavaScript 很容易做到:
function sayHiBye(firstName, lastName) {
// helper nested function to use below
function getFullName() {
return firstName + " " + lastName;
}
alert( "Hello, " + getFullName() );
alert( "Bye, " + getFullName() );
}
嵌套函數(shù) getFullName() 可以訪問外部變量,幫助我們很方便地返回 FullName 。
更有趣的是,嵌套函數(shù)可以被 return ,作為一個新對象的屬性或者作為自己的結(jié)果。這樣它們就能在其他地方使用,無論在哪里,它都能訪問同樣的外部變量。
一個構(gòu)造函數(shù)(詳見 info:constructor-new)的例子:
// 構(gòu)造函數(shù)返回一個新對象
function User(name) {
// 嵌套函數(shù)創(chuàng)造對象方法
this.sayHi = function() {
alert(name);
};
}
let user = new User("John");
user.sayHi(); // 方法返回外部 "name"
一個 return 函數(shù)的例子:
function makeCounter() {
let count = 0;
return function() {
return count++; // has access to the outer counter
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
我們接著研究 makeCounter 。counter 函數(shù)每調(diào)用一次就會返回下一個數(shù)。盡管這很簡單,但只要輕微修改,它便具有一定的實用性,例如偽隨機數(shù)生成器。
counter 內(nèi)部如何工作?
內(nèi)部函數(shù)運行, count++ 中的變量由內(nèi)到外搜索:

- 嵌套函數(shù)局部變量……
- 外層函數(shù)……
- 直到全局變量。
第二步我們找到了 count 。當外部變量被修改,它所在的地方就被修改。所以 count++ 檢索外部變量并對其加一是操作于該變量自己的 Lexical Environment 。就像操作了 let count = 1 一樣。
這里需要思考兩個問題:
- 我們能通過
makeCounter以外的方法重置counter嗎? - 如果我們可以多次調(diào)用
makeCounter(),返回了很多counter函數(shù),他們的count是獨立的還是共享的?
繼續(xù)閱讀前可以先嘗試思考一下。
...
ok ?
那我們開始揭曉謎底:
- 沒門。
counter是局部變量,不可能在外部直接訪問。 - 每次調(diào)用
makeCounter()都會新建 Lexical Environment,每一個環(huán)境都有自己的counter。所以不同 counter 里的count是獨立的。
一個 demo :
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter1 = makeCounter();
let counter2 = makeCounter();
alert( counter1() ); // 0
alert( counter1() ); // 1
alert( counter2() ); // 0 (獨立)
現(xiàn)在你能清楚外部變量的使用,但是你仍然需要更深入地理解以面對更復雜的情況,現(xiàn)在我們進入下一步。
Environment 細節(jié)
對 closure (閉包)有了初步了解之后,可以開始深入細節(jié)了。
下面是 makeCounter 例子的動作分解,跟著看你就能理解一切了。注意, [[Environment]] 屬性我們之前還未介紹。
-
腳本開始運行,此時只存在 global Lexical Environment :
image這時候只有
makeCounter一個函數(shù),這是函數(shù)聲明,還未被調(diào)用。所有函數(shù)都帶著一個隱藏屬性
[[Environment]]“誕生”。[[Environment]]指向它們創(chuàng)建的 Lexical Environment 。是[[Environment]]讓函數(shù)知道它“誕生”于什么環(huán)境。makeCounter創(chuàng)建于 global Lexical Environment ,所以[[Environment]]指向它。換句話說,Lexical Environment 在函數(shù)誕生時就“銘刻”在這個函數(shù)中。
[[Environment]]是指向 Lexical Environment 的隱藏函數(shù)屬性。 -
代碼繼續(xù)走,
makeCounter()登上舞臺。這是代碼運行到makeCounter()瞬間的快照:imagemakeCounter()調(diào)用時,保存當前變量和實參的 Lexical Environment 已經(jīng)被創(chuàng)建。Lexical Environment 儲存 2 個東西:
- 帶有局部變量的 Environment Record 。例子中
count是唯一的局部變量(let count被執(zhí)行的時候記錄)。 - 被綁定到函數(shù)
[[Environment]]的外部詞法引用。例子里makeCounter的[[Environment]]引用了 global Lexical Environment 。
所以這里有兩個 Lexical Environments :全局,和
makeCounter(outer 引用全局)。 - 帶有局部變量的 Environment Record 。例子中
-
在
makeCounter()執(zhí)行的過程中,創(chuàng)建了一個嵌套函數(shù)。這無關(guān)于函數(shù)創(chuàng)建使用的是 Function Declaration (函數(shù)聲明)還是 Function Expression (函數(shù)表達式)。所有函數(shù)都會得到引用他們被創(chuàng)建時 Lexical Environment 的
[[Environment]]屬性。這個嵌套函數(shù)的
[[Environment]]是makeCounter()(它的誕生地)的 Lexical Environment:image同樣注意,這一步是函數(shù)聲明而非調(diào)用。
-
代碼繼續(xù)執(zhí)行,
makeCounter()調(diào)用結(jié)束,內(nèi)嵌函數(shù)被賦值到全局變量counter:image這個函數(shù)只有一行:
return count++。 -
counter()被調(diào)用,自動創(chuàng)建一個 “空” Lexical Environment 。 此函數(shù)無局部變量,但是[[Environment]]引用了外面一層,所以它可以訪問makeCounter()的變量。image要訪問變量,先檢索自己的 Lexical Environment (empty),然后是
makeCounter()的,最后是全局的。例子中在最近的外層 Lexical EnvironmentmakeCounter中發(fā)現(xiàn)了count。重點來了,內(nèi)存在這里是怎么管理的?盡管
makeCounter()調(diào)用結(jié)束了,它的 Lexical Environment 依然保存在內(nèi)存中,這是因為嵌套函數(shù)的[[Environment]]引用了它。通常, Lexical Environment 對象隨著使用它的函數(shù)的存在而存在。沒有函數(shù)引用它的時候,它才會被清除。
-
counter()函數(shù)不只是返回count,還會對其 +1 操作。這個修改已經(jīng)在“適當?shù)奈恢谩蓖瓿闪恕?code>count 的值在它被找到的環(huán)境中被修改。image這一步出了返回了新的
count,其他完全相同。(譯者:總結(jié)一下,聲明時記錄環(huán)境 [[Environment]](函數(shù)所在環(huán)境),執(zhí)行時創(chuàng)建詞法環(huán)境(局部+outer 就是引用 [[Environment]] ),而閉包就是函數(shù) + 它的詞法環(huán)境,所以定義上來說所有函數(shù)都是閉包,但是之后被返回出來可以使用的閉包才是“實用意義”上的閉包)
下一個
counter()調(diào)用操作同上。
本章開頭第二個問題的答案現(xiàn)在顯而易見了。
以下代碼的 work() 函數(shù)通過外層 lexical environment 引用了它原地點的 name :

所以這里的答案是 "Pete" 。
但是如果 makeWorker() 沒了 let name ,如我們所見,作用域搜索會到達外層,獲取全局變量。這個情況下答案會是 "John" 。
閉包 (Closure)
開發(fā)者們都應該知道編程領(lǐng)域的通用名詞閉包 (closure)。
Closure 是一個記錄并可訪問外層變量的函數(shù)。在一些編程語言中,這是不可能的,或者要以一種特殊的方式書寫以實現(xiàn)這個功能。但是如上面解釋的, JavaScript 的所有函數(shù)都(很自然地)是個閉包。(有一個例外,詳見info:new-function)
這就是閉包:它們使用[[Environment]]屬性自動記錄各自的創(chuàng)建地點,然后由此訪問外部變量。
在前端面試中,如果面試官問你什么是閉包,正確答案應該包括閉包的定義,以及解釋為何 JavaScript 的所有函數(shù)都是閉包,最好可以再簡單說說里面的技術(shù)細節(jié):[[Environment]]屬性和 Lexical Environments 的原理。
代碼塊、循環(huán)、 IIFE
上面的例子都著重于函數(shù),但是 Lexical Environment 也存在于代碼塊 {...} 。
它們在代碼塊運行時創(chuàng)建,包含塊局部變量。這里有一些例子。
If
下例中,當執(zhí)行到 if 塊,會為這個塊創(chuàng)建新的 "if-only" Lexical Environment :

與函數(shù)同樣原理,塊內(nèi)可以找到 phrase ,但是塊外不能使用塊內(nèi)的變量和函數(shù)。如果執(zhí)意在 if 外面用 user ,那只能得到一個報錯了。
For, while
對于循環(huán),每個 iteration 都會有自己的 Lexical Environment ,在 for 里定義的變量,也是塊的局部變量,也屬于塊的 Lexical Environment :
for (let i = 0; i < 10; i++) {
// Each loop has its own Lexical Environment
// {i: value}
}
alert(i); // Error, no such variable
let i 只在塊內(nèi)可用,每次循環(huán)都有它自己的 Lexical Environment ,每次循環(huán)都會帶著當前的 i ,最后循環(huán)結(jié)束, i 不可用。
代碼塊
我們也可以直接用 {…} 把變量隔離到一個“局部作用域”(local scope)。
在瀏覽器中所有 script 共享全局變量,這就很容易造成變量的重名、覆蓋。
為了避免這種情況我們可以使用代碼塊隔離自己的代碼:
{
// do some job with local variables that should not be seen outside
let message = "Hello";
alert(message); // Hello
}
alert(message); // Error: message is not defined
代碼塊有自己的 Lexical Environment ,塊外無法訪問塊內(nèi)變量。
IIFE
以前沒有代碼塊,要實現(xiàn)上述效果要依靠所謂的“立即執(zhí)行函數(shù)表達式”(immediately-invoked function expressions ,縮寫 IIFE):
(function() {
let message = "Hello";
alert(message); // Hello
})();
這個函數(shù)表達式創(chuàng)建后立即執(zhí)行,這段代碼立即執(zhí)行并有自己的私有變量。
函數(shù)表達式需要被括號包裹。 JavaScript 執(zhí)行時遇到 "function" 會理解為一個函數(shù)聲明,函數(shù)聲明必須有名稱,沒有就會報錯:
// Error: Unexpected token (
function() { // <-- JavaScript cannot find function name, meets ( and gives error
let message = "Hello";
alert(message); // Hello
}();
你可能會說:“那我給他加個名字咯”,但這依然行不通,JavaScript 不允許函數(shù)聲明立刻被執(zhí)行:
// syntax error because of brackets below
function go() {
}(); // <-- can't call Function Declaration immediately
圓括號告訴 JavaScript 這個函數(shù)創(chuàng)建于其他表達式的上下文,因此這是個函數(shù)表達式。不需要名稱,也可以立即執(zhí)行。
也有其他方法告訴 JavaScript 我們需要的是函數(shù)表達式:
// 創(chuàng)建 IIFE 的方法
(function() {
alert("Brackets around the function");
})();
(function() {
alert("Brackets around the whole thing");
}());
!function() {
alert("Bitwise NOT operator starts the expression");
}();
+function() {
alert("Unary plus starts the expression");
}();
垃圾回收
Lexical Environment 對象與普通的值的內(nèi)存管理規(guī)則是一樣的。
-
通常 Lexical Environment 在函數(shù)運行完畢就會被清理:
function f() { let value1 = 123; let value2 = 456; } f();這兩個值是 Lexical Environment 的屬性,但是
f()執(zhí)行完后,這個 Lexical Environment 無任何變量引用(unreachable),所以它會從內(nèi)存刪除。 -
...但是如果有內(nèi)嵌函數(shù),它的
[[Environment]]會引用f的 Lexical Environment(reachable):function f() { let value = 123; function g() { alert(value); } return g; } let g = f(); // g is reachable, and keeps the outer lexical environment in memory -
注意,
f()如果被多次調(diào)用,返回的函數(shù)都被保存,相應的 Lexical Environment 會分別保存在內(nèi)存:function f() { let value = Math.random(); return function() { alert(value); }; } // 3 functions in array, every one of them links to Lexical Environment // from the corresponding f() run // LE LE LE let arr = [f(), f(), f()]; -
Lexical Environment 對象在不被引用 (unreachable) 后被清除: 無嵌套函數(shù)引用它。下例中,
g自身不被引用后,value也會被清除:function f() { let value = 123; function g() { alert(value); } return g; } let g = f(); // while g is alive // there corresponding Lexical Environment lives g = null; // ...and now the memory is cleaned up
現(xiàn)實中的優(yōu)化
理論上,函數(shù)還在,它的所有外部變量都會被保留。
但在實踐中,JavaScript 引擎可能會對此作出優(yōu)化,引擎在分析變量的使用情況后,把沒有使用的外部變量刪除。
在 V8 (Chrome, Opera) 有個問題,這些被刪除的變量不能在 debugger 觀察了。
嘗試在 Chrome Developer Tools 運行以下代碼:
function f() {
let value = Math.random();
function g() {
debugger; // 在 console 輸入 alert( value ); 發(fā)現(xiàn)無此變量!
}
return g;
}
let g = f();
g();
你可以看到,這里沒有保存 value 變量!理論上它應該是可訪問的,但是引擎優(yōu)化移除了這個變量。
還有一個有趣的 debug 問題。下面的代碼 alert 出外面的同名變量而不是里面的:
let value = "Surprise!";
function f() {
let value = "the closest value";
function g() {
debugger; // in console: type alert( value ); Surprise!
}
return g;
}
let g = f();
g();
再會!
如果你用 Chrome/Opera 來debug ,很快就能發(fā)現(xiàn)這個 V8 feature。
這不是 bug 而是 V8 feature,或許將來會被修改。至于改沒改,運行一下上面的例子就能判斷啦。





