來源於 現代JavaScript教程
閉包章節
中文翻譯計劃
本文很清晰地解釋了閉包是什麼,以及閉包如何產生,相信你看完也會有所收穫
關鍵字
Closure 閉包
Lexical Environment 詞法環境
Environment Record 環境記錄
outer Lexical Environment 外部詞法環境
global Lexical Environment 全局語法環境javascript
JavaScript 是一個 function-oriented(直譯:面向函數)的語言,這個特性爲咱們帶來了很大的操做自由。函數只需建立一次,賦值到一個變量,或者做爲參數傳入另外一個函數而後在一個全新的環境調用。前端
函數能夠訪問它外部的變量,是一個經常使用 feature。java
可是當外部變量改變時會發生什麼?函數會獲取最新的值,仍是函數建立當時的值?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(變量儲存點),箭頭表明一個外部引用。global Lexical Environment 沒有外部引用,因此指向 null
。
下圖展現 let
變量的工做機制:
右邊的正方形描述 global Lexical Environment 在執行中如何改變:
let phrase
定義出現了。由於沒有賦值因此儲存爲 undefined
。phrase
被賦值。phrase
被賦新值。看起來很簡單對不對?
總結:
Function Declaration 與 let
不一樣,並不是處理於被執行的時候,而是(譯者注:意思就是全局詞法環境建立時處理函數聲明)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
,因此要循着 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()
能夠訪問外部變量,幫助咱們很方便地返回組合後的全名。
更有趣的是,嵌套函數能夠做爲一個新對象的屬性或者本身自己被 return。這樣它們就能在其餘地方使用,不管在哪裏,它都能訪問一樣的外部變量。
一個構造函數的例子:
// 構造函數返回一個新對象 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++
中的變量由內到外搜索:
第 2 步咱們找到了 count
。外部變量會直接在其所在的地方被修改。因此 count++
檢索外部變量,並在該變量本身的 Lexical Environment 進行 +1 操做。就像操做了 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]]
屬性咱們以前還沒有介紹。
這時候只有 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 Environment:全局,和 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(空),而後是 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)。
閉包是一個記錄並可訪問外層變量的函數。在一些編程語言中是不存在的,或者要以一種特殊的方式書寫以實現這個功能。可是如上面解釋的,JavaScript 的全部函數都個閉包。
這就是閉包:它們使用[[Environment]]
屬性自動記錄各自的建立地點,而後由此訪問外部變量。
在前端面試中,若是面試官問你什麼是閉包,正確答案應該包括閉包的定義,以及解釋爲什麼 JavaScript 的全部函數都是閉包,最好能夠再簡單說說裏面的技術細節:[[Environment]]
屬性和 Lexical Environments 的原理。
上面的例子都着重於函數,可是 Lexical Environment 也存在於代碼塊 {...}
。
它們在代碼塊運行時建立,包含塊局部變量。這裏有一些例子。
下例中,當執行到 if
塊,會爲這個塊建立新的 "if-only" Lexical Environment :
與函數一樣原理,塊內能夠找到 phrase
,可是塊外不能使用塊內的變量和函數。若是執意在 if
外面用 user
,那隻能獲得一個報錯了。
對於循環,每一次迭代都會有本身的 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,或許未來會被修改。至於改沒改,運行一下上面的例子就能判斷啦。