摘要:嵌套函數在一個函數中創建另一個函數,稱為嵌套。這在很容易實現嵌套函數可以訪問外部變量,幫助我們很方便地返回組合后的全名。更有趣的是,嵌套函數可以作為一個新對象的屬性或者自己本身被。
來源于 現代JavaScript教程
閉包章節
中文翻譯計劃
本文很清晰地解釋了閉包是什么,以及閉包如何產生,相信你看完也會有所收獲
關鍵字
Closure 閉包
Lexical Environment 詞法環境
Environment Record 環境記錄
outer Lexical Environment 外部詞法環境
global Lexical Environment 全局語法環境
JavaScript 是一個 function-oriented(直譯:面向函數)的語言,這個特性為我們帶來了很大的操作自由。函數只需創建一次,賦值到一個變量,或者作為參數傳入另一個函數然后在一個全新的環境調用。
函數可以訪問它外部的變量,是一個常用 feature。
但是當外部變量改變時會發生什么?函數會獲取最新的值,還是函數創建當時的值?
還有一個問題,當函數被傳入其他地方再調用……他能訪問那個地方的外部變量嗎?
不同語言的表現有所不同,下面我們研究一下 JavaScript 中的表現。
兩個問題我們先思考下面兩種情況,看完這篇文章你就可以回答這兩個問題,更復雜的問題也不在話下。
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"(調用時)?Lexical Environment(詞法環境)
要理解里面發生了什么,必須先明白“變量”到底是什么。
在 JavaScript 里,任何運行的函數、代碼塊、整個 script 都會關聯一個被稱為 Lexical Environment (詞法環境) 的對象。
Lexical Environment 對象包含兩個部分:(譯者:這里是重點)
Environment Record(環境記錄)是一個以全部局部變量為屬性的對象(以及其他如 this 值的信息)。
對 outer lexical environment(外部詞法環境)的引用,通常關聯詞法上的外面一層代碼(花括號外一層)。
所以,“變量”就是內部對象 Environment Record 的一個屬性。要獲取或改變一個對象,意味著獲取改變 Lexical Environment 的屬性。
例如在這段簡單的代碼中,只有一個 Lexical Environment:
這就是所謂 global Lexical Environment(全局語法環境),對應整個 script。對于瀏覽端,整個 標簽共享一個全局環境。
(譯者:這里是重點)
上圖中,正方形代表 Environment Record(變量儲存點),箭頭代表一個外部引用。global Lexical Environment 沒有外部引用,所以指向 null。
下圖展示 let 變量的工作機制:
右邊的正方形描述 global Lexical Environment 在執行中如何改變:
腳本開始運行,Lexical Environment 為空。
let phrase 定義出現了。因為沒有賦值所以儲存為 undefined 。
phrase 被賦值。
phrase 被賦新值。
看起來很簡單對不對?
總結:
變量是一個特殊內部對象的屬性,關聯于執行時的塊、函數、script 。
對變量的操作實際上是對這個對象屬性的操作。
Function Declaration(函數聲明)Function Declaration 與 let 不同,并非處理于被執行的時候,而是(譯者注:意思就是全局詞法環境創建時處理函數聲明)Lexical Environment 創建的時候。對于 global Lexical Environment,意味著 script 開始運行的時候。
這就是函數可以在定義前調用的原因。
以下代碼 Lexical Environment 開始時非空。因為有 say 函數聲明,之后又有了 let 聲明的 phrase:
Inner and outer Lexical Environment(內部詞法環境和外部詞法環境)調用 say() 的過程中,它使用了外部變量,一起看看這里面發生了什么。
(譯者:這里是重點)
函數運行時會自動創建一個新的函數 Lexical Environment。這是所有函數的通用規則。這個新的 Lexical Environment 用于當前運行函數的存放局部變量和形參。
箭頭標記的是執行 say("John") 時的 Lexical Environment :
函數調用過程中,可以看到兩個 Lexical Environment(譯者注:就是兩個長方形):里面的是函數調用產生的,外面的是全局的:
內層 Lexical Environment 對應當前執行的 say。它只有一個變量:函數實參 name。我們調用了 say("John"),所以 name 的值是 "John"。
外層 Lexical Environment 是 global Lexical Environment。
內層 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
上述代碼的執行流:
global Lexical Environment 存在 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 (獨立)
現在你清楚明白了外部變量的情況,但是面對更復雜的情況仍然需要更深入地理解,讓我們進入下一步吧。
Environment 細節對 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 個東西:
帶有局部變量的 Environment Record。例子中 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)代碼塊、循環、 IIFE
開發者們都應該知道編程領域的通用名詞閉包(closure)。
閉包是一個記錄并可訪問外層變量的函數。在一些編程語言中是不存在的,或者要以一種特殊的方式書寫以實現這個功能。但是如上面解釋的,JavaScript 的所有函數都個閉包。
這就是閉包:它們使用 [[Environment]] 屬性自動記錄各自的創建地點,然后由此訪問外部變量。
在前端面試中,如果面試官問你什么是閉包,正確答案應該包括閉包的定義,以及解釋為何 JavaScript 的所有函數都是閉包,最好可以再簡單說說里面的技術細節:[[Environment]] 屬性和 Lexical Environments 的原理。
上面的例子都著重于函數,但是 Lexical Environment 也存在于代碼塊 {...} 。
它們在代碼塊運行時創建,包含塊局部變量。這里有一些例子。
If下例中,當執行到 if 塊,會為這個塊創建新的 "if-only" Lexical Environment :
與函數同樣原理,塊內可以找到 phrase,但是塊外不能使用塊內的變量和函數。如果執意在 if 外面用 user,那只能得到一個報錯了。
For, while對于循環,每一次迭代都會有自己的 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 ,塊外無法訪問塊內變量。
IIFE以前沒有代碼塊,要實現上述效果要依靠所謂的“立即執行函數表達式”(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,或許將來會被修改。至于改沒改,運行一下上面的例子就能判斷啦。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107967.html
摘要:插件開發前端掘金作者原文地址譯者插件是為應用添加全局功能的一種強大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內優雅的實現文件分片斷點續傳。 Vue.js 插件開發 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應用添加全局功能的一種強大而且簡單的方式。插....
摘要:然而學習布局,你只要學習幾個手機端頁面自適應解決方案布局進階版附源碼示例前端掘金一年前筆者寫了一篇手機端頁面自適應解決方案布局,意外受到很多朋友的關注和喜歡。 十分鐘學會 Fiddler - 后端 - 掘金一.Fiddler介紹 Fiddler是一個http抓包改包工具,fiddle英文中有欺騙、偽造之意,與wireshark相比它更輕量級,上手簡單,因為只能抓http和https數據...
摘要:函數式編程前端掘金引言面向對象編程一直以來都是中的主導范式。函數式編程是一種強調減少對程序外部狀態產生改變的方式。 JavaScript 函數式編程 - 前端 - 掘金引言 面向對象編程一直以來都是JavaScript中的主導范式。JavaScript作為一門多范式編程語言,然而,近幾年,函數式編程越來越多得受到開發者的青睞。函數式編程是一種強調減少對程序外部狀態產生改變的方式。因此,...
摘要:使用上一篇文章的例子來說明下自由變量進階期深入淺出圖解作用域鏈和閉包訪問外部的今天是今天是其中既不是參數,也不是局部變量,所以是自由變量。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第二期,本周的主題是作用域閉包,今天是第7天。 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進階計...
閱讀 1808·2021-11-23 09:51
閱讀 1268·2021-11-18 10:02
閱讀 963·2021-10-25 09:44
閱讀 2099·2019-08-26 18:36
閱讀 1619·2019-08-26 12:17
閱讀 1146·2019-08-26 11:59
閱讀 2746·2019-08-23 15:56
閱讀 3350·2019-08-23 15:05