來源於 現代JavaScript教程
閉包章節
中文翻譯計劃
本文很清晰地解釋了閉包是什麼,以及閉包如何產生,相信你看完也會有所收穫javascript
關鍵字 Closure 閉包 Lexical Environment 詞法環境 Environment Record 環境記錄前端
JavaScript 是一個 function-oriented 的語言。這帶來了很大的操做自由。函數只需建立一次,能夠拷貝到另外一個變量,或者做爲一個參數傳入另外一個函數而後在一個全新的環境調用。java
咱們知道函數能夠訪問它外部的變量,這個 feature 十分經常使用。git
可是當外部變量改變時會發生什麼?函數時獲取最新的值,仍是函數建立當時的值?github
還有一個問題,當函數被送到其餘地方再調用……他能訪問那個地方的外部變量嗎?面試
不一樣語言的表現有所不一樣,下面咱們研究一下 JavaScript 中的表現。express
咱們先思考下面兩種狀況,看完這篇文章你就能夠回答這兩個問題,更復雜的問題也不在話下。編程
sayHi
函數使用了外部變量 name
。函數運行時,會使用兩個值中的哪一個?瀏覽器
let name = "John";
function sayHi() {
alert("Hi, " + name);
}
name = "Pete";
sayHi(); // "John" 仍是 "Pete"?
複製代碼
這個狀況不管是瀏覽器端仍是服務器端都很常見。函數極可能在它建立一段時間後才執行,例如等待用戶操做或者網絡請求。服務器
問題是:函數是否會選擇變量最新的值呢?
makeWorker
函數創造並返回了另外一個函數。這個新函數能夠在任何地方調用。他會訪問建立時的變量仍是調用時的變量呢?
function makeWorker() {
let name = "Pete";
return function() {
alert(name);
};
}
let name = "John";
// 建立函數
let work = makeWorker();
// 調用函數
work(); // "Pete" (建立時) 仍是 "John" (調用時)?
複製代碼
要理解裏面發生了什麼,必須先明白「變量」究竟是什麼。
在 JavaScript 裏,任何運行的函數、代碼塊、整個 script 都會關聯一個被叫作 Lexical Environment (詞法環境) 的對象。
Lexical Environment 對象包含兩個部分:(譯者:這裏是重點)
this
值的信息)。因此,「變量」就是內部對象 Environment Record 的一個屬性。要改變一個對象,意味着改變 Lexical Environment 的屬性。
例如在這段簡單的代碼中,只有一個 Lexical Environment:
這就是所謂 global Lexical Environment (全局語法環境),對應整個 script。對於瀏覽端,整個 <script>
標籤共享一個全局環境。
(譯者:這裏是重點) 上圖中,正方形表明 Environment Record (變量儲存),箭頭表明 outer reference (外部引用)。global Lexical Environment 沒有外部引用,因此指向 null
。
下圖展現 let
變量的工做機制:
右邊的正方形描述 global Lexical Environment 在執行中如何改變:
let phrase
定義出現了。由於沒有賦值因此儲存爲 undefined
。phrase
被賦值。phrase
被賦新值。看起來很簡單對不對?
總結:
Function Declaration 並不是處理於被執行的時候,而是 Lexical Environment 建立的時候。對於 global Lexical Environment ,這意味着 script 開始運行的時候。
這就是函數能夠在定義前調用的緣由。
如下代碼 Lexical Environment 開始時非空。由於有 say
函數聲明,以後又有了 let
聲明的 phrase
:
調用 say()
的過程當中,它使用了外部變量,一塊兒看看這裏面發生了什麼。
(譯者:這裏是重點) 函數運行時會自動建立一個新的函數 Lexical Environment 。這是全部函數的通用規則。這個新的 Lexical Environment 用於當前運行函數的存放局部變量和形參。
箭頭標記的是執行 say("John")
時的 Lexical Environment :
函數調用過程當中,能夠看到兩個 Lexical Environment :裏面的是函數調用產生的,外面的是全局的:
say
。它只有一個變量: 函數實參 name
。咱們調用 say("John")
,因此 name
的值是 "John"
。內層 Lexical Environment 有一個 outer
屬性,指向外層 Lexical Environment。
代碼要訪問一個變量,首先搜索內層 Lexical Environment ,接着是外層,再外層,直到鏈的結束。
若是走完整條鏈變量都找不到,在 strict mode 就會報錯了。不使用 use strict
的狀況下,對未定義變量的賦值,會創造一個新的全局變量。
下面一塊兒看看變量搜索如何處理:
say
裏的 alert
想要訪問 name
,當即就能在當前函數的 Lexical Environment 找到。phrase
,局部變量不存在 phrase
,因此要循着 outer
在全局變量裏找到。如今咱們能夠回答本章開頭的第一個問題了。
函數獲取外部變量當前值
舊變量值不儲存在任何地方,函數須要他們的時候,它取得來源於自身或外部 Lexical Environment 的當前值。
因此第一個問題的答案是 Pete
:
let name = "John";
function sayHi() {
alert("Hi, " + name);
}
name = "Pete"; // (*)
sayHi(); // Pete
複製代碼
上述代碼的執行流:
name: "John"
。(*)
行中,全局變量修改了,如今成了這樣 name: "Pete"
。say()
執行的時候, 取外部 name
。此時在 global Lexical Environment 中已是 "Pete"
。一次調用,一個 Lexical Environment
請注意,每當一個函數運行,就會建立一個新的 function Lexical Environment。
若是一個函數被屢次調用,那麼每次調用都會生成一個屬於當前調用的全新 Lexical Environment ,裏面裝載着當前調用的變量和實參。
Lexical Environment 是一個標準對象 (specification object)
"Lexical Environment" 是一個標準對象 (specification object)。咱們不能直接獲取或設置它,JavaScript 引擎也可能優化它,拋棄未使用的變量來節省內存或者做其餘優化,可是可見行爲應該如上面所述。
在一個函數中建立另外一個函數,稱爲「嵌套」。這在 JavaScript 很容易作到:
function sayHiBye(firstName, lastName) {
// helper nested function to use below
function getFullName() {
return firstName + " " + lastName;
}
alert( "Hello, " + getFullName() );
alert( "Bye, " + getFullName() );
}
複製代碼
嵌套函數 getFullName()
能夠訪問外部變量,幫助咱們很方便地返回 FullName 。
更有趣的是,嵌套函數能夠被 return ,做爲一個新對象的屬性或者做爲本身的結果。這樣它們就能在其餘地方使用,不管在哪裏,它都能訪問一樣的外部變量。
一個構造函數(詳見 info:constructor-new)的例子:
// 構造函數返回一個新對象
function User(name) {
// 嵌套函數創造對象方法
this.sayHi = function() {
alert(name);
};
}
let user = new User("John");
user.sayHi(); // 方法返回外部 "name"
複製代碼
一個 return 函數的例子:
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 函數每調用一次就會返回下一個數。儘管這很簡單,但只要輕微修改,它便具備必定的實用性,例如僞隨機數生成器。
counter 內部如何工做?
內部函數運行, count++
中的變量由內到外搜索:
第二步咱們找到了 count
。當外部變量被修改,它所在的地方就被修改。因此 count++
檢索外部變量並對其加一是操做於該變量本身的 Lexical Environment 。就像操做了 let count = 1
同樣。
這裏須要思考兩個問題:
makeCounter
之外的方法重置 counter
嗎?makeCounter()
,返回了不少 counter
函數,他們的 count
是獨立的仍是共享的?繼續閱讀前能夠先嚐試思考一下。
...
ok ?
那咱們開始揭曉謎底:
counter
是局部變量,不可能在外部直接訪問。makeCounter()
都會新建 Lexical Environment,每個環境都有本身的 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 (獨立)
複製代碼
如今你能清楚外部變量的使用,可是你仍然須要更深刻地理解以面對更復雜的狀況,如今咱們進入下一步。
對 closure (閉包)有了初步瞭解以後,能夠開始深刻細節了。
下面是 makeCounter
例子的動做分解,跟着看你就能理解一切了。注意, [[Environment]]
屬性咱們以前還未介紹。
腳本開始運行,此時只存在 global Lexical Environment :
這時候只有 makeCounter
一個函數,這是函數聲明,還未被調用。
全部函數都帶着一個隱藏屬性 [[Environment]]
「誕生」。 [[Environment]]
指向它們建立的 Lexical Environment 。是[[Environment]]
讓函數知道它「誕生」於什麼環境。
makeCounter
建立於 global Lexical Environment ,因此 [[Environment]]
指向它。
換句話說,Lexical Environment 在函數誕生時就「銘刻」在這個函數中。[[Environment]]
是指向 Lexical Environment 的隱藏函數屬性。
代碼繼續走, makeCounter()
登上舞臺。這是代碼運行到 makeCounter()
瞬間的快照:
makeCounter()
調用時,保存當前變量和實參的 Lexical Environment 已經被建立。
Lexical Environment 儲存 2 個東西:
count
是惟一的局部變量( let count
被執行的時候記錄)。[[Environment]]
的外部詞法引用。例子裏 makeCounter
的 [[Environment]]
引用了 global Lexical Environment 。因此這裏有兩個 Lexical Environments :全局,和 makeCounter
(outer 引用全局)。
在 makeCounter()
執行的過程當中,建立了一個嵌套函數。
這無關於函數建立使用的是 Function Declaration (函數聲明)仍是 Function Expression (函數表達式)。全部函數都會獲得引用他們被建立時 Lexical Environment 的 [[Environment]]
屬性。
這個嵌套函數的 [[Environment]]
是 makeCounter()
(它的誕生地)的 Lexical Environment:
一樣注意,這一步是函數聲明而非調用。
代碼繼續執行,makeCounter()
調用結束,內嵌函數被賦值到全局變量 counter
:
這個函數只有一行: return count++
。
counter()
被調用,自動建立一個 「空」 Lexical Environment 。 此函數無局部變量,可是 [[Environment]]
引用了外面一層,因此它能夠訪問 makeCounter()
的變量。
要訪問變量,先檢索本身的 Lexical Environment (empty),而後是 makeCounter()
的,最後是全局的。例子中在最近的外層 Lexical Environment makeCounter
中發現了 count
。
重點來了,內存在這裏是怎麼管理的?儘管 makeCounter()
調用結束了,它的 Lexical Environment 依然保存在內存中,這是由於嵌套函數的 [[Environment]]
引用了它。
一般, Lexical Environment 對象隨着使用它的函數的存在而存在。沒有函數引用它的時候,它纔會被清除。
counter()
函數不僅是返回 count
,還會對其 +1 操做。這個修改已經在「適當的位置」完成了。count
的值在它被找到的環境中被修改。
這一步出了返回了新的 count
,其餘徹底相同。
(譯者:總結一下,聲明時記錄環境 [[Environment]](函數所在環境),執行時建立詞法環境(局部+outer 就是引用 [[Environment]] ),而閉包就是函數 + 它的詞法環境,因此定義上來講全部函數都是閉包,可是以後被返回出來可使用的閉包纔是「實用意義」上的閉包)
下一個 counter()
調用操做同上。
本章開頭第二個問題的答案如今顯而易見了。
如下代碼的 work()
函數經過外層 lexical environment 引用了它原地點的 name
:
因此這裏的答案是 "Pete"
。
可是若是 makeWorker()
沒了 let name
,如咱們所見,做用域搜索會到達外層,獲取全局變量。這個狀況下答案會是 "John"
。
閉包 (Closure)
開發者們都應該知道編程領域的通用名詞閉包 (closure)。
Closure 是一個記錄並可訪問外層變量的函數。在一些編程語言中,這是不可能的,或者要以一種特殊的方式書寫以實現這個功能。可是如上面解釋的, JavaScript 的全部函數都(很天然地)是個閉包。(有一個例外,詳見info:new-function)
這就是閉包:它們使用[[Environment]]
屬性自動記錄各自的建立地點,而後由此訪問外部變量。
在前端面試中,若是面試官問你什麼是閉包,正確答案應該包括閉包的定義,以及解釋爲什麼 JavaScript 的全部函數都是閉包,最好能夠再簡單說說裏面的技術細節:[[Environment]]
屬性和 Lexical Environments 的原理。
上面的例子都着重於函數,可是 Lexical Environment 也存在於代碼塊 {...}
。
它們在代碼塊運行時建立,包含塊局部變量。這裏有一些例子。
下例中,當執行到 if
塊,會爲這個塊建立新的 "if-only" Lexical Environment :
與函數一樣原理,塊內能夠找到 phrase
,可是塊外不能使用塊內的變量和函數。若是執意在 if
外面用 user
,那隻能獲得一個報錯了。
對於循環,每一個 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
只在塊內可用,每次循環都有它本身的 Lexical Environment ,每次循環都會帶着當前的 i
,最後循環結束, 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 ,塊外沒法訪問塊內變量。
之前沒有代碼塊,要實現上述效果要依靠所謂的「當即執行函數表達式」(immediately-invoked function expressions ,縮寫 IIFE):
(function() {
let message = "Hello";
alert(message); // Hello
})();
複製代碼
這個函數表達式建立後當即執行,這段代碼當即執行並有本身的私有變量。
函數表達式須要被括號包裹。 JavaScript 執行時遇到 "function"
會理解爲一個函數聲明,函數聲明必須有名稱,沒有就會報錯:
// Error: Unexpected token (
function() { // <-- JavaScript cannot find function name, meets ( and gives error
let message = "Hello";
alert(message); // Hello
}();
複製代碼
你可能會說:「那我給他加個名字咯」,但這依然行不通,JavaScript 不容許函數聲明馬上被執行:
// syntax error because of brackets below
function go() {
}(); // <-- can't call Function Declaration immediately
複製代碼
圓括號告訴 JavaScript 這個函數建立於其餘表達式的上下文,所以這是個函數表達式。不須要名稱,也能夠當即執行。
也有其餘方法告訴 JavaScript 咱們須要的是函數表達式:
// 建立 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 對象與普通的值的內存管理規則是同樣的。
一般 Lexical Environment 在函數運行完畢就會被清理:
function f() {
let value1 = 123;
let value2 = 456;
}
f();
複製代碼
這兩個值是 Lexical Environment 的屬性,可是 f()
執行完後,這個 Lexical Environment 無任何變量引用(unreachable),因此它會從內存刪除。
...可是若是有內嵌函數,它的 [[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()
若是被屢次調用,返回的函數都被保存,相應的 Lexical Environment 會分別保存在內存:
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) 後被清除: 無嵌套函數引用它。下例中, 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
複製代碼
理論上,函數還在,它的全部外部變量都會被保留。
但在實踐中,JavaScript 引擎可能會對此做出優化,引擎在分析變量的使用狀況後,把沒有使用的外部變量刪除。
在 V8 (Chrome, Opera) 有個問題,這些被刪除的變量不能在 debugger 觀察了。
嘗試在 Chrome Developer Tools 運行如下代碼:
function f() {
let value = Math.random();
function g() {
debugger; // 在 console 輸入 alert( value ); 發現無此變量!
}
return g;
}
let g = f();
g();
複製代碼
你能夠看到,這裏沒有保存 value
變量!理論上它應該是可訪問的,可是引擎優化移除了這個變量。
還有一個有趣的 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 ,很快就能發現這個 V8 feature。 這不是 bug 而是 V8 feature,或許未來會被修改。至於改沒改,運行一下上面的例子就能判斷啦。