摘要:不過(guò)到底是怎麼保留的另外為什麼一個(gè)閉包可以一直使用區(qū)域變數(shù),即便這些變數(shù)在該內(nèi)已經(jīng)不存在了為了解開(kāi)閉包的神秘面紗,我們將要假裝沒(méi)有閉包這東西而且也不能夠用嵌套來(lái)重新實(shí)作閉包。
原文出處: 連結(jié)
話說(shuō)網(wǎng)路上有很多文章在探討閉包(Closures)時(shí)大多都是簡(jiǎn)單的帶過(guò)。大多的都將閉包的定義濃縮成一句簡(jiǎn)單的解釋?zhuān)蔷褪?b>一個(gè)閉包是一個(gè)函數(shù)能夠保留其建立時(shí)的執(zhí)行環(huán)境。不過(guò)到底是怎麼保留的?
另外為什麼一個(gè)閉包可以一直使用區(qū)域變數(shù),即便這些變數(shù)在該 scope 內(nèi)已經(jīng)不存在了?
為了解開(kāi)閉包的神秘面紗,我們將要假裝 Javascript 沒(méi)有閉包這東西而且也不能夠用嵌套 function 來(lái)重新實(shí)作閉包。這麼做我們將會(huì)發(fā)現(xiàn)閉包真實(shí)的本質(zhì)是什麼以及在底層到底是怎麼運(yùn)作的。
為了這個(gè)練習(xí)我們同時(shí)也需要假裝 Javascript 本身具備了另一個(gè)不存在的功能。那就是一個(gè)原始的物件當(dāng)它如果被當(dāng)成 function 調(diào)用的時(shí)候是可以執(zhí)行的。
你可能已經(jīng)在其他語(yǔ)言中看過(guò)這個(gè)功能,在 Python 中你可以定義一個(gè) __call__ 方法,在 PHP 則有一個(gè)特殊的方法叫 __invoke
這些方法(Method)會(huì)在當(dāng)物件被當(dāng)作 function 調(diào)用時(shí)執(zhí)行。如果我們假裝 Javascript 也有這個(gè)功能,我們可能需要這麼實(shí)作:
let o = { n: 42, __call__() { return this.n; } }; // 當(dāng)我們把物件當(dāng)作 function 一樣調(diào)用時(shí) o(); // 42, 當(dāng)然現(xiàn)在你會(huì)得到 `TypeError: o is not a function` 的錯(cuò)誤
這邊我們得到一個(gè)普通的物件,我們假裝我們可以把它當(dāng)做 function 來(lái)呼叫,然後當(dāng)我們這個(gè)做的同時(shí)其實(shí)我們是執(zhí)行一個(gè)特殊的方法 __call__ 如果你真的要實(shí)作記得用 o.__call__()。
譯者註: 注意! 假如您想實(shí)作的時(shí),呼叫 可調(diào)用物件 例如上面的 o() 都要換成 o.__call__()
現(xiàn)在讓我們先來(lái)看看一個(gè)簡(jiǎn)單的閉包範(fàn)例。
function f() { // 下面這個(gè)變數(shù)是 f() 的區(qū)域變數(shù) // 通常,當(dāng)我們離開(kāi) f 的 scope 時(shí),這個(gè)變數(shù) n 就應(yīng)該要被回收了 let n = 42; // 嵌套的 function 參考了 n function g() { return n; } return g; } // 讓我們透過(guò) f() 來(lái)建立一個(gè) g 函數(shù) let g = f(); // 理論上這個(gè)變數(shù) n 在 f() 執(zhí)行完畢之後就應(yīng)該要立即被回收,對(duì)吧? // 畢竟 f 已經(jīng)執(zhí)行完畢了,而且我們也離開(kāi)了該 scope // 那為什麼 g 可以繼續(xù)參考一個(gè)已經(jīng)被釋放的變數(shù)呢? g(); // 42
外層的 function f 有一個(gè)區(qū)域變數(shù),然後裡面的 function g 參考 f 的區(qū)域變數(shù)。
接著我們把內(nèi)層的 g 回傳指派給 f scope 外的變數(shù)。但我們好奇的是如果 f 執(zhí)行完畢被釋放了,那為什麼 g 仍然可以取得已被釋放的 f 的區(qū)域變數(shù)呢?
這個(gè)的魔法便是 - 一個(gè)閉包不僅僅只是一個(gè) function。它是一個(gè)物件,具有建構(gòu)子和私有資料。然後我們可以它當(dāng)作 function 來(lái)使用。
那如果 Javascript 沒(méi)有閉包這種用法,我們必須自己實(shí)作它呢?這就是我們接下來(lái)要看到的。
// 譯者註: 這邊請(qǐng)先不要糾結(jié) Babel 或其他 Complier 實(shí)際編譯出來(lái)的 ES5 還是用 function class G { constructor(n) { this._n = n } __call__() { return this._n; } } function f() { let n = 42; // 這就是一個(gè)閉包 // 這個(gè)內(nèi)層的 function 其實(shí)不只是一個(gè) function // 它其實(shí)是一個(gè)可以被調(diào)用的物件,然後我們傳入 n 到它的建構(gòu)子 let g = new G(n); return g; } // 透過(guò)呼叫 f() 取得一個(gè)可以被調(diào)用的物件 g let g = f(); // 現(xiàn)在就算原來(lái)從 f 拿到的區(qū)域變數(shù) n 被回收了也沒(méi)關(guān)係 // 可被調(diào)用的物件 g 實(shí)際上是參考自己私有的資料 g(); // 42
如果您曾看過(guò) ECMAScript 規(guī)範(fàn),可能會(huì)對(duì)實(shí)際上是參考自己私有的資料這句話產(chǎn)生一些疑問(wèn),先別急著否定。這邊不過(guò)是試著用另外一個(gè)較淺的角度解釋。
這邊我們把內(nèi)部的 function g 用一個(gè) G class 的實(shí)例物件(即 new 出來(lái)的物件) 取代,然後我們透過(guò)把 f 的區(qū)域變數(shù) n 傳進(jìn) G 的建構(gòu)子,藉此將變數(shù)儲(chǔ)存在新的實(shí)例物件私有的資料中。最終我們可以取得 f 的區(qū)域變數(shù)(n)。
OK! 各位觀眾這就是一個(gè)閉包的行為。閉包就是一個(gè)可調(diào)用的物件,可以把透過(guò)建構(gòu)子把傳入的參數(shù)保留在私有的空間中。
讓我們?cè)偕钊胍稽c(diǎn)聰明的讀者已經(jīng)發(fā)現(xiàn)還有一些行為還沒(méi)解釋清楚或者說(shuō)我們的模擬實(shí)作是有漏洞的。讓我們來(lái)觀察其他的閉包範(fàn)例
function f() { let n = 42; // 內(nèi)部函數(shù)取得變數(shù) n function get() { return n; } // 另外一個(gè)內(nèi)部函數(shù)也同時(shí)存取 n function next() { return n++; } return { get, next }; } let o = f(); o.get(); // 42 o.next(); o.get(); // 43
在這個(gè)範(fàn)例中,我們得到兩個(gè)閉包同時(shí)參考變數(shù) n 。其中一個(gè)函數(shù)的操作變數(shù)會(huì)影響另外一個(gè)變數(shù)取得得值。
但如果 Javascript 沒(méi)有閉包,單靠我們上面的實(shí)作和 JS 的行為將不會(huì)一樣。
class Get { constructor(n) { this._n = n; } __call__() { return this._n; } } class Next { constructor(n) { this._n = n; } __call__() { this._n++; } } function f() { let n = 42; // 這邊的閉包我們一樣換成可調(diào)用的物件 // 它們可以將參數(shù)傳入建構(gòu)子,進(jìn)而將值保留起來(lái) let get = new Get(n); let next = new Next(n); return { get, next }; } let o = f(); o.get(); // 42 o.next(); o.get(); // 42
跟上面一樣,我們?nèi)〈藘?nèi)部 function get 和 next 的部分改成使用物件。它們是透過(guò)將值保留在物件內(nèi)部進(jìn)而取得 f 的區(qū)域變數(shù),每一個(gè)物件具有自己私有的資料。同時(shí)我們也注意到其中一個(gè)可調(diào)用物件 操作 n 並不會(huì)影響另外一個(gè)。這是因?yàn)樗鼈兪莻?n 的值 value而不是傳址 reference/address。白話文就是複製了一分資料。並不是操作變數(shù)本身。
為了要解釋為什麼 Javascript 的閉包會(huì)參考到相同的 n 即記憶體位置是一樣的。我們需要解釋變數(shù)本身。在底層,Javascript 的區(qū)域變數(shù)跟我們從其他語(yǔ)言理解的觀念並不相同,它們是負(fù)責(zé)動(dòng)態(tài)分配與計(jì)算參考(reference)的物件的屬性,稱(chēng)為 LexicalEnvironment 物件。Javascript 的閉包其實(shí)會(huì)有一個(gè)參考指向到整個(gè) 執(zhí)行環(huán)境, 上下文, Context 的 LexicalEnvironment 物件,而不是特定的變數(shù)。
如果您對(duì)於 scope 與 context 還不是很了解強(qiáng)烈建議您觀賞這篇
讓我們來(lái)修改我們的可調(diào)用物件讓其可以取得一個(gè) lexical environment 而不是 n 。
class Get { constructor(lexicalEnvironment) { this._lexicalEnvironment = lexicalEnvironment; } __call__() { return this._lexicalEnvironment.n; } } class Next { constructor(lexicalEnvironment) { this._lexicalEnvironment = lexicalEnvironment; } __call__() { this._lexicalEnvironment.n++; } } function f() { let lexicalEnvironment = { n: 42 } // 現(xiàn)在這個(gè)可調(diào)用變數(shù)是透過(guò)一個(gè)參考 lexical environment 來(lái)改變 n // 所以現(xiàn)在變更的是同一個(gè) n 了 let get = new Get(lexicalEnvironment); let next = new Next(lexicalEnvironment); return { get, next } } // 現(xiàn)在我們實(shí)作的物件行為跟 javascript 一致了 // 還是請(qǐng)注意如果您要時(shí)作,記得 o.get() 要換成 o.get.__call__() 喔 let o = f(); o.get(); // 42 o.next(); o.get(); // 43
上面實(shí)作我們將區(qū)域變數(shù) n 換成 lexicalEnvironment 物件,然後具有一個(gè)屬性 n 。
這時(shí) Get 和 Next 的物件實(shí)例所存取的便是同一個(gè)參考(reference)即 lexical environment 物件。
所以現(xiàn)在修改的就是相同的地方了?;旧线@就是一個(gè)閉包的行為。
閉包是一個(gè)物件而且當(dāng)它們是函數(shù)時(shí)我們可以直接調(diào)用。而事實(shí)上任何一個(gè) Javascript 中的函數(shù)都是一個(gè)可被調(diào)用的物件也稱(chēng)作 function object 或者 functor 當(dāng)它們被執(zhí)行或者說(shuō)被實(shí)例化時(shí)會(huì)帶有一個(gè)私有的 lexical environment 物件。而想要更了解關(guān)於這個(gè)物件的看官們可以參考Lexical environment的定義。
在 Javascript 不是 function 創(chuàng)造閉包,function 本身就是一個(gè)閉包。
老實(shí)說(shuō)譯者本身還是比較喜歡理解 context 與 variable object 的說(shuō)明,接著用 一個(gè)閉包是一個(gè)函數(shù)能夠保留其建立時(shí)的執(zhí)行環(huán)境 這句話來(lái)記憶。雖然翻譯了這篇文章但讓小弟對(duì)於閉包有更深入理解是這篇解讀ECMAScript[1]——執(zhí)行環(huán)境、作用域及閉包。不過(guò)原作者從這個(gè)角度來(lái)解釋的確是可以概略的理解整個(gè)運(yùn)作機(jī)制,希望這篇文章能讓你有所收穫。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/78739.html
摘要:現(xiàn)在,我們可以開(kāi)始探討介面的設(shè)計(jì)模式了。匯出命名空間一個(gè)簡(jiǎn)單且常用的設(shè)計(jì)模式就是匯出一個(gè)包含數(shù)個(gè)屬性的物件,這些屬性具體的內(nèi)容主要是函式,但並不限於函式。如此,我們就能夠透過(guò)匯入該模組來(lái)取得這個(gè)命名空間下一系列相關(guān)的功能。 前言 這篇文章試著要整理,翻譯Export This: Interface Design Patterns for Node.js Modules這篇非常值得一讀的...
摘要:整體來(lái)說(shuō)網(wǎng)頁(yè)主要是由矩形所構(gòu)成的,另一方面印刷品則具備相對(duì)多樣性。即便我們?cè)O(shè)定的元素不再是矩形,但周?chē)脑嘏帕蟹绞饺匀痪S持原本矩形的佈局。為了達(dá)成周?chē)脑馗们械男螤?,我們可以使用屬性。周?chē)脑厝孕枰縼?lái)修正。 整體來(lái)說(shuō)網(wǎng)頁(yè)主要是由矩形所構(gòu)成的,另一方面印刷品則具備相對(duì)多樣性。造成這樣差異的原因有很多,不過(guò)其中一個(gè)即是缺少合適的工具。 這篇文章主要會(huì)介紹 clip-path 這...
摘要:的架構(gòu)設(shè)計(jì)促使第三方開(kāi)發(fā)者讓核心發(fā)揮出無(wú)限的潛力。當(dāng)然建置比起開(kāi)發(fā)是較進(jìn)階的議題,因?yàn)槲覀儽仨氁斫鈨?nèi)部的一些事件。這個(gè)編譯結(jié)果包含的訊息包含模組的狀態(tài),編譯後的資源檔,發(fā)生異動(dòng)的檔案,被觀察的相依套件等。 本文將對(duì) webpack 周邊的 middleware 與 plugin 套件等作些介紹,若您對(duì)於 webpack 還不了解可以參考這篇彙整的翻譯。 webpack dev ser...
摘要:載入流程被限制在兩個(gè)階段根據(jù)上面的模式,內(nèi)嵌透過(guò)隱藏尚未套用樣式的內(nèi)容,然後非同步得載入之後呈現(xiàn)內(nèi)容。樣式表本身的載入機(jī)制是平行的,但是套用樣式卻是要照順序的。我們需要一點(diǎn)小技巧來(lái)避免。 這週閱讀到這篇有意思的文章,於是便動(dòng)手寫(xiě)下簡(jiǎn)單的翻譯,如果有理解錯(cuò)誤的地方歡迎指教。 Chrome 正在試圖改變當(dāng) 寫(xiě)在 的行為,從blink-dev 的文章並不能很清楚的知道其優(yōu)點(diǎn)。所以這篇文章...
摘要:不過(guò)這個(gè)效果感覺(jué)上就像是閃一下就切換到該位置。為了使用體驗(yàn)上的感覺(jué)有時(shí)候網(wǎng)站會(huì)設(shè)計(jì)一種平滑捲動(dòng)到該位置的效果。的方式非常簡(jiǎn)單,只要在該元素設(shè)定注意是而不是這個(gè)方式非常方便不過(guò)目前只有支援,查閱。 眾所皆知 HTML 錨點(diǎn)(anchor link)透過(guò)給定標(biāo)籤 id 屬性跳到頁(yè)面上特定位置的功能。不過(guò)這個(gè)效果感覺(jué)上就像是閃一下就切換到該位置。為了使用體驗(yàn)上的感覺(jué)有時(shí)候網(wǎng)站會(huì)設(shè)計(jì)一種平滑捲...
閱讀 1427·2021-10-14 09:43
閱讀 1019·2021-09-10 10:51
閱讀 1479·2021-09-01 10:42
閱讀 2226·2019-08-30 15:55
閱讀 610·2019-08-30 15:55
閱讀 2374·2019-08-30 14:21
閱讀 1749·2019-08-30 13:04
閱讀 3502·2019-08-29 13:09