摘要:相像閉包和對象之間的關(guān)系可能不是那么明顯。一個沒有對象的編程語言可以用閉包來模擬對象。事實上,表達(dá)一個對象為閉包形式,或閉包為對象形式是相當(dāng)簡單的。簡而言之,閉包和對象是狀態(tài)的同構(gòu)表示及其相關(guān)功能。
原文地址:Functional-Light-JS
原文作者:Kyle Simpson-《You-Dont-Know-JS》作者
第 7 章: 閉包 vs 對象關(guān)于譯者:這是一個流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅實的梁柱;分享,是 CSS 里最閃耀的一瞥;總結(jié),是 JavaScript 中最嚴(yán)謹(jǐn)?shù)倪壿嫛=?jīng)過捶打磨練,成就了本書的中文版。本書包含了函數(shù)式編程之精髓,希望可以幫助大家在學(xué)習(xí)函數(shù)式編程的道路上走的更順暢。比心。
譯者團(tuán)隊(排名不分先后):阿希、blueken、brucecham、cfanlife、dail、kyoko-df、l3ve、lilins、LittlePineapple、MatildaJin、冬青、pobusama、Cherry、蘿卜、vavd317、vivaxy、萌萌、zhouyao
數(shù)年前,Anton van Straaten 創(chuàng)造了一個非常有名且被常常引用的 禪理 來舉例和證實一個閉包和對象之間重要的關(guān)系。
德高望重的大師 Qc Na 曾經(jīng)和他的學(xué)生 Anton 一起散步。Anton 希望引導(dǎo)大師到一個討論里,說到:大師,我曾聽說對象是一個非常好的東西,是這樣么?Qc Na 同情地看著他的學(xué)生回答到, “愚笨的弟子,對象只不過是可憐人的閉包”
被批評后,Anton 離開他的導(dǎo)師并回到了自己的住處,致力于學(xué)習(xí)閉包。他認(rèn)真的閱讀整個“匿名函數(shù):終極……”系列論文和它的姐妹篇,并且實踐了一個基于閉包系統(tǒng)的小的 Scheme 解析器。他學(xué)了很多,盼望展現(xiàn)給他導(dǎo)師他的進(jìn)步。
當(dāng)他下一次與 Qc Na 一同散步時,Anton 試著提醒他的導(dǎo)師,說到 “導(dǎo)師,我已經(jīng)勤奮地學(xué)習(xí)了這件事,我現(xiàn)在明白了對象真的是可憐人的閉包。” ,Qc Na 用棍子戳了戳 Anton 回應(yīng)到,“你什么時候才能學(xué)會,閉包才是可憐人的對象”。在那一刻, Anton 明白了什么。
Anton van Straaten 6/4/2003
http://people.csail.mit.edu/g...
原帖盡管簡短,卻有更多關(guān)于起源和動機(jī)的內(nèi)容,我強烈推薦為了理解本章去閱讀原帖來調(diào)整你的觀念。
我觀察到很多人讀完這個會對其中的聰明智慧傻笑,卻繼續(xù)不改變他們的想法。但是,這個禪理(來自 Bhuddist Zen 觀點)促使讀者進(jìn)入其中對立真相的辯駁中。所以,返回并且再讀一遍。
到底是哪個?是閉包是可憐的對象,還是對象是可憐的閉包?或都不是?或都是?或者這只是為了說明閉包和對象在某些方面是相同的方式?
還有它們中哪個與函數(shù)式編程相關(guān)?拉一把椅子過來并且仔細(xì)考慮一會兒。如果你愿意,這一章將是一個精彩的迂回之路,一個遠(yuǎn)足。
達(dá)成共識先確定一點,當(dāng)我們談及閉包和對象我們都達(dá)成了共識。我們顯然是在 JavaScript 如何處理這兩種機(jī)制的上下文中進(jìn)行討論的,并且特指的是討論簡單函數(shù)閉包(見第 2 章的“保持作用域”)和簡單對象(鍵值對的集合)。
一個簡單的函數(shù)閉包:
function outer() { var one = 1; var two = 2; return function inner(){ return one + two; }; } var three = outer(); three(); // 3
一個簡單的對象:
var obj = { one: 1, two: 2 }; function three(outer) { return outer.one + outer.two; } three( obj ); // 3
但提到“閉包“時,很多人會想很多額外的事情,例如異步回調(diào)甚至是封裝和信息隱藏的模塊模式。同樣,”對象“會讓人想起類、this、原型和大量其它的工具和模式。
隨著深入,我們會需要小心地處理部分額外的相關(guān)內(nèi)容,但是現(xiàn)在,盡量只記住閉包和對象最簡單的釋義 —— 這會減少很多探索過程中的困惑。
相像閉包和對象之間的關(guān)系可能不是那么明顯。讓我們先來探究它們之間的相似點。
為了給這次討論一個基調(diào),讓我簡述兩件事:
一個沒有閉包的編程語言可以用對象來模擬閉包。
一個沒有對象的編程語言可以用閉包來模擬對象。
換句話說,我們可以認(rèn)為閉包和對象是一樣?xùn)|西的兩種表達(dá)方式。
狀態(tài)思考下面的代碼:
function outer() { var one = 1; var two = 2; return function inner(){ return one + two; }; } var obj = { one: 1, two: 2 };
inner() 和 obj 對象持有的作用域都包含了兩個元素狀態(tài):值為 1 的 one 和值為 2 的 two。從語法和機(jī)制來說,這兩種聲明狀態(tài)是不同的。但概念上,他們的確相當(dāng)相似。
事實上,表達(dá)一個對象為閉包形式,或閉包為對象形式是相當(dāng)簡單的。接下來,嘗試一下:
var point = { x: 10, y: 12, z: 14 };
你是不是想起了一些相似的東西?
function outer() { var x = 10; var y = 12; var z = 14; return function inner(){ return [x,y,z]; } }; var point = outer();
注意: 每次被調(diào)用時 inner() 方法創(chuàng)建并返回了一個新的數(shù)組(亦然是一個對象)。這是因為 JS 不提供返回多個數(shù)據(jù)卻不包裝在一個對象中的能力。這并不是嚴(yán)格意義上的一個違反我們對象類似閉包的說明的任務(wù),因為這只是一個暴露/運輸具體值的實現(xiàn),狀態(tài)追蹤本身仍然是基于對象的。使用 ES6+ 數(shù)組解構(gòu),我們可以聲明地忽視這個臨時中間對象通過另一種方式:var [x,y,z] = point()。從開發(fā)者工程學(xué)角度,值應(yīng)該被多帶帶存儲并且通過閉包而不是對象來追蹤。
如果你有一個嵌套對象會怎么樣?
var person = { name: "Kyle Simpson", address: { street: "123 Easy St", city: "JS"ville", state: "ES" } };
我們可以用嵌套閉包來表示相同的狀態(tài):
function outer() { var name = "Kyle Simpson"; return middle(); // ******************** function middle() { var street = "123 Easy St"; var city = "JS"ville"; var state = "ES"; return function inner(){ return [name,street,city,state]; }; } } var person = outer();
讓我們嘗試另一個方向,從閉包轉(zhuǎn)為對象:
function point(x1,y1) { return function distFromPoint(x2,y2){ return Math.sqrt( Math.pow( x2 - x1, 2 ) + Math.pow( y2 - y1, 2 ) ); }; } var pointDistance = point( 1, 1 ); pointDistance( 4, 5 ); // 5
distFromPoint(..) 封裝了 x1 和 y1,但是我們也可以通過傳入一個具體的對象作為替代值:
function pointDistance(point,x2,y2) { return Math.sqrt( Math.pow( x2 - point.x1, 2 ) + Math.pow( y2 - point.y1, 2 ) ); }; pointDistance( { x1: 1, y1: 1 }, 4, // x2 5 // y2 ); // 5
明確地傳入point 對象替換了閉包的隱式狀態(tài)。
行為,也是一樣!對象和閉包不僅是表達(dá)狀態(tài)集合的方式,而且他們也可以包含函數(shù)或者方法。將數(shù)據(jù)和行為捆綁為有一個充滿想象力的名字:封裝。
思考:
function person(name,age) { return happyBirthday(){ age++; console.log( "Happy " + age + "th Birthday, " + name + "!" ); } } var birthdayBoy = person( "Kyle", 36 ); birthdayBoy(); // Happy 37th Birthday, Kyle!
內(nèi)部函數(shù) happyBirthday() 封閉了 name 和 age ,所以內(nèi)部的函數(shù)也持有了這個狀態(tài)。
我們也可以通過 this 綁定一個對象來獲取同樣的能力:
var birthdayBoy = { name: "Kyle", age: 36, happyBirthday() { this.age++; console.log( "Happy " + this.age + "th Birthday, " + this.name + "!" ); } }; birthdayBoy.happyBirthday(); // Happy 37th Birthday, Kyle!
我們?nèi)匀煌ㄟ^ happyBrithday() 函數(shù)來表達(dá)對狀態(tài)數(shù)據(jù)的封裝,但是用對象代替了閉包。同時我們沒有顯式給函數(shù)傳遞一個對象(如同先前的例子);JavaScript 的 this 綁定可以創(chuàng)造一個隱式的綁定。
從另一方面分析這種關(guān)系:閉包將單個函數(shù)與一系列狀態(tài)結(jié)合起來,而對象卻在保有相同狀態(tài)的基礎(chǔ)上,允許任意數(shù)量的函數(shù)來操作這些狀態(tài)。
事實上,我們可以在一個作為接口的閉包上將一系列的方法暴露出來。思考一個包含了兩個方法的傳統(tǒng)對象:
var person = { firstName: "Kyle", lastName: "Simpson", first() { return this.firstName; }, last() { return this.lastName; } } person.first() + " " + person.last(); // Kyle Simpson
只用閉包而不用對象,我們可以表達(dá)這個程序為:
function createPerson(firstName,lastName) { return API; // ******************** function API(methodName) { switch (methodName) { case "first": return first(); break; case "last": return last(); break; }; } function first() { return firstName; } function last() { return lastName; } } var person = createPerson( "Kyle", "Simpson" ); person( "first" ) + " " + person( "last" ); // Kyle Simpson
盡管這些程序看起來感覺有點反人類,但它們實際上只是相同程序的不同實現(xiàn)。
(不)可變許多人最初都認(rèn)為閉包和對象行為的差別源于可變性;閉包會阻止來自外部的變化而對象則不然。但是,結(jié)果是,這兩種形式都有典型的可變行為。
正如第 6 章討論的,這是因為我們關(guān)心的是值的可變性,值可變是值本身的特性,不在于在哪里或者如何被賦值的。
function outer() { var x = 1; var y = [2,3]; return function inner(){ return [ x, y[0], y[1] ]; }; } var xyPublic = { x: 1, y: [2,3] };
在 outer() 中字面變量 x 存儲的值是不可變的 —— 記住,定義的基本類型如 2 是不可變的。但是 y 的引用值,一個數(shù)組,絕對是可變的。這點對于 xyPublic 中的 x 和 y 屬性也是完全相同的。
通過指出 y 本身是個數(shù)組我們可以強調(diào)對象和閉包在可變這點上沒有關(guān)系,因此我們需要將這個例子繼續(xù)拆解:
function outer() { var x = 1; return middle(); // ******************** function middle() { var y0 = 2; var y1 = 3; return function inner(){ return [ x, y0, y1 ]; }; } } var xyPublic = { x: 1, y: { 0: 2, 1: 3 } };
如果你認(rèn)為這個如同 “世界是一只馱著一只一直馱下去的烏龜(對象)群”,在最底層,所有的狀態(tài)數(shù)據(jù)都是基本類型,而所有基本類型都是不可變值。
不論是用嵌套對象還是嵌套閉包代表狀態(tài),這些被持有的值都是不可變的。
同構(gòu)同構(gòu)這個概念最近在 JavaScript 圈經(jīng)常被提出,它通常被用來指代碼可以同時被服務(wù)端和瀏覽器端使用/分享。我不久以前寫了一篇博文說明這種對同構(gòu)這個詞的使用是錯誤的,隱藏了它實際上確切和重要的意思。
這里我是博文部分的節(jié)選:
同構(gòu)的意思是什么?當(dāng)然,我們可以用數(shù)學(xué)詞匯,社會學(xué)或者生物學(xué)討論它。同構(gòu)最普遍的概念是你有兩個類似但是不相同的結(jié)構(gòu)。
在這些所有的慣用法中,同構(gòu)和相等的區(qū)別在這里:如果兩個值在各方面完全一致那么它們相等,但是如果它們表現(xiàn)不一致卻仍有一對一或者雙向映射的關(guān)系那么它們是同構(gòu)。
換而言之,兩件事物A和B如果你能夠映射(轉(zhuǎn)化)A 到 B 并且能夠通過反向映射回到A那么它們就是同構(gòu)。
回想第 2 章的簡單數(shù)學(xué)回顧,我們討論了函數(shù)的數(shù)學(xué)定義是一個輸入和輸出之間的映射。我們指出這在學(xué)術(shù)上稱為態(tài)射。同構(gòu)是雙映(雙向)態(tài)射的特殊案例,它需要映射不僅僅必須可以從任意一邊完成,而且在任一方式下反應(yīng)完全一致。
不去思考這些關(guān)于數(shù)字的問題,讓我們將同構(gòu)關(guān)聯(lián)到代碼。再一次引用我的博文:
如果 JS 有同構(gòu)的話是怎么樣的?它可能是一集合的 JS 代碼轉(zhuǎn)化為了另一集合的 JS 代碼,并且(重要的是)如果你原意的話,你可以把轉(zhuǎn)化后的代碼轉(zhuǎn)為之前的。
正如我們之前通過閉包如同對象和對象如同閉包為例聲稱的一樣,它們的表達(dá)可以任意替換。就這一點來說,它們互為同構(gòu)。
簡而言之,閉包和對象是狀態(tài)的同構(gòu)表示(及其相關(guān)功能)。
下次你聽到誰說 “X 與 Y 是同構(gòu)的”,他們的意思是,“X 和 Y 可以從兩者中的任意一方轉(zhuǎn)化到另一方,并且無論怎樣都保持了相同的特性。”
內(nèi)部結(jié)構(gòu)所以,我們可以從我們寫的代碼角度想象對象是閉包的一種同構(gòu)展示。但我們也可以觀察到閉包系統(tǒng)可以被實現(xiàn),并且很可能是用對象實現(xiàn)的!
這樣想一下:在如下的代碼中, 在 outer() 已經(jīng)運行后,JS 如何為了 inner() 的引用保持對變量 x 的追蹤?
function outer() { var x = 1; return function inner(){ return x; }; }
我們會想到作用域,outer() 作為屬性的對象實施設(shè)置所有的變量定義。因此,從概念上講,在內(nèi)存中的某個地方,是類似這樣的。
scopeOfOuter = { x: 1 };
接下來對于 inner() 函數(shù),一旦創(chuàng)建,它獲得了一個叫做 scopeOfInner 的(空)作用域?qū)ο螅@個對象被其 [[Prototype]] 連接到 scopeOfOuter 對象,近似這個:
scopeOfInner = {}; Object.setPrototypeOf( scopeOfInner, scopeOfOuter );
接著,當(dāng)內(nèi)部的 inner() 建立詞法變量 x 的引用時,實際更像這樣:
return scopeOfInner.x;
scopeOfInner 并沒有一個 x 的屬性,當(dāng)他的 [[Prototype]] 連接到擁有 x 屬性的 scopeOfOuter時。通過原型委托訪問 scopeOfOuter.x 返回值是 1。
這樣,我們可以近似認(rèn)為為什么 outer() 的作用域甚至在當(dāng)它執(zhí)行完都被保留(通過閉包),這是因為 scopeOfInner 對象連接到 scopeOfOuter 對象,因此,使這個對象和它的屬性完整的被保存下來。
現(xiàn)在,這都只是概念。我沒有從字面上說 JS 引擎使用對象和原型。但它完全有道理,它可以同樣地工作。
許多語言實際上通過對象實現(xiàn)了閉包。另一些語言用閉包的概念實現(xiàn)了對象。但我們讓讀者使用他們的想象力思考這是如何工作的。
同根異枝所以閉包和對象是等價的,對嗎?不完全是,我打賭它們比你在讀本章前想的更加相似,但是它們?nèi)杂兄匾膮^(qū)別點。
這些區(qū)別點不應(yīng)當(dāng)被視作缺點或者不利于使用的論點;這是錯誤的觀點。對于給定的任務(wù),它們應(yīng)該被視為使一個或另一個更適合(和可讀)的特點和優(yōu)勢。
結(jié)構(gòu)可變性從概念上講,閉包的結(jié)構(gòu)不是可變的。
換而言之,你永遠(yuǎn)不能從閉包添加或移除狀態(tài)。閉包是一個表示對象在哪里聲明的特性(被固定在編寫/編譯時間),并且不受任何條件的影響 —— 當(dāng)然假設(shè)你使用嚴(yán)格模式并且/或者沒有使用作弊手段例如 eval(..)。
注意: JS 引擎可以從技術(shù)上過濾一個對象來清除其作用域中不再被使用的變量,但是這是一個對于開發(fā)者透明的高級的優(yōu)化。無論引擎是否實際做了這類優(yōu)化,我認(rèn)為對于開發(fā)者來說假設(shè)閉包是作用域優(yōu)先而不是變量優(yōu)先是最安全的。如果你不想保留它,就不要封閉它(在閉包里)!
但是,對象默認(rèn)是完全可變的,你可以自由的添加或者移除(delete)一個對象的屬性/索引,只要對象沒有被凍結(jié)(Object.freeze(..))
這或許是代碼可以根據(jù)程序中運行時條件追蹤更多(或更少)狀態(tài)的優(yōu)勢。
舉個例子,讓我們思考追蹤游戲中的按鍵事件。幾乎可以肯定,你會考慮使用一個數(shù)組來做這件事:
function trackEvent(evt,keypresses = []) { return keypresses.concat( evt ); } var keypresses = trackEvent( newEvent1 ); keypresses = trackEvent( newEvent2, keypresses );
注意:你能否認(rèn)出為什么我使用 concat(..) 而不是直接對 keypresses 數(shù)組使用 push(..) 操作?因為在函數(shù)式編程中,我們通常希望對待數(shù)組如同不可變數(shù)據(jù)結(jié)構(gòu),可以被創(chuàng)建和添加,但不能直接改變。我們剔除了顯式重新賦值帶來的邪惡副作用(稍后再作說明)。
盡管我們不在改變數(shù)組的結(jié)構(gòu),但當(dāng)我們希望時我們也可以。稍后詳細(xì)介紹。
數(shù)組不是記錄這個 evt 對象的增長“列表”的僅有的方式。。我們可以使用閉包:
function trackEvent(evt,keypresses = () => []) { return function newKeypresses() { return [ ...keypresses(), evt ]; }; } var keypresses = trackEvent( newEvent1 ); keypresses = trackEvent( newEvent2, keypresses );
你看出這里發(fā)生了什么嗎?
每次我們添加一個新的事件到這個“列表”,我們創(chuàng)建了一個包裝了現(xiàn)有 keypresses() 方法(閉包)的新閉包,這個新閉包捕獲了當(dāng)前的 evt 。當(dāng)我們調(diào)用 keypresses() 函數(shù),它將成功地調(diào)用所有的內(nèi)部方法,并創(chuàng)建一個包含所有獨立封裝的 evt 對象的中間數(shù)組。再次說明,閉包是一個追蹤所有狀態(tài)的機(jī)制;這個你看到的數(shù)組只是一個對于需要一個方法來返回函數(shù)中多個值的具體實現(xiàn)。
所以哪一個更適合我們的任務(wù)?毫無意外,數(shù)組方法可能更合適一些。閉包的不可變結(jié)構(gòu)意味著我們的唯一選項是封裝更多的閉包在里面。對象默認(rèn)是可擴(kuò)展的,所以我們需要增長這個數(shù)組就足夠了。
順便一提,盡管我們表現(xiàn)出結(jié)構(gòu)不可變或可變是一個閉包和對象之間的明顯區(qū)別,然而我們使用對象作為一個不可變數(shù)據(jù)的方法實際上使之更相似而非不同。
數(shù)組每次添加就創(chuàng)造一個新數(shù)組(通過 concat(..))就是把數(shù)組對待為結(jié)構(gòu)不可變,這個概念上對等于通過適當(dāng)?shù)脑O(shè)計使閉包結(jié)構(gòu)上不可變。
私有當(dāng)對比分析閉包和對象時可能你思考的第一個區(qū)分點就是閉包通過詞法作用域提供“私有”狀態(tài),而對象將一切做為公共屬性暴露。這種私有有一個精致的名字:信息隱藏。
考慮詞法閉包隱藏:
function outer() { var x = 1; return function inner(){ return x; }; } var xHidden = outer(); xHidden(); // 1
現(xiàn)在同樣的狀態(tài)公開:
var xPublic = { x: 1 }; xPublic.x; // 1
這里有一些在常規(guī)的軟件工程原理方面明顯的區(qū)別 —— 考慮下抽象,這種模塊模式有著公有和私有 API 等等。但是讓我們試著把我們的討論局限于函數(shù)式編程的觀點,畢竟,這是一本關(guān)于函數(shù)式編程的書!
可見性似乎隱藏信息的能力是一種理想狀態(tài)的跟蹤特性,但是我認(rèn)為函數(shù)式編程者可能持反對觀點。
在一個對象中管理狀態(tài)作為公開屬性的一個優(yōu)點是這使你狀態(tài)中的所有數(shù)據(jù)更容易枚舉(迭代)。思考下你想訪問每一個按鍵事件(從之前的那個例子)并且存儲到一個數(shù)據(jù)庫,使用一個這樣的工具:
function recordKeypress(keypressEvt) { // 數(shù)據(jù)庫實用程序 DB.store( "keypress-events", keypressEvt ); }
If you already have an array -- just an object with public numerically-named properties -- this is very straightforward using a built-in JS array utility forEach(..):
如果你已經(jīng)有一個數(shù)組,正好是一個擁有公開的用數(shù)字命名屬性的對象 —— 非常直接地使用 JS 對象的內(nèi)建工具 forEach(..):
keypresses.forEach( recordKeypress );
但是,如果按鍵列表被隱藏在一個閉包里,你不得不在閉包內(nèi)暴露一個享有特權(quán)訪問數(shù)據(jù)的公開 API 工具。
舉例而說,我可以給我們的閉包 —— keypresses 例子自有的 forEach 方法,如同數(shù)組內(nèi)建的:
function trackEvent( evt, keypresses = { list() { return []; }, forEach() {} } ) { return { list() { return [ ...keypresses.list(), evt ]; }, forEach(fn) { keypresses.forEach( fn ); fn( evt ); } }; } // .. keypresses.list(); // [ evt, evt, .. ] keypresses.forEach( recordKeypress );
對象狀態(tài)數(shù)據(jù)的可見性讓我們能更直接地使用它,而閉包遮掩狀態(tài)讓我們更艱難地處理它。
Change Control 變更控制如果詞法變量被隱藏在一個閉包中,只有閉包內(nèi)部的代碼才能自由的重新賦值,在外部修改 x 是不可能的。
正如我們在第 6 章看到的,提升代碼可讀性的唯一真相就是減少表面掩蓋,讀者必須可以預(yù)見到每一個給定變量的行為。
詞法(作用域)在重新賦值上的局部就近原則是為什么我不認(rèn)為 const 是一個有幫助的特性的一個重要原因。作用域(例如閉包)通常應(yīng)該盡可能小,這意味著重新賦值只會影響少許代碼。在上面的 outer() 中,我們可以快速地檢查到?jīng)]有一行代碼重設(shè)了 x,至此(x 的)所有意圖和目的表現(xiàn)地像一個常量。
這類保證對于我們對函數(shù)純凈的信任是一個強有力的貢獻(xiàn),例如。
換而言之,xPublic.x 是一個公開屬性,程序的任何部分都能引用 xPublic ,默認(rèn)有重設(shè) xPublic.x 到別的值的能力。這會讓很多行代碼需要被考慮。
這是為什么在第 6 章, 我們視 Object.freeze(..) 為使所有的對象屬性只讀(writable: false)的一個快速而凌亂的方式,讓它們不能被不可預(yù)測的重設(shè)。
不幸的是,Object.freeze(..) 是極端且不可逆的。
使有了閉包,你就有了一些可以更改代碼的權(quán)限,而剩余的程序是受限的。當(dāng)我們凍結(jié)一個對象,代碼中沒有任何部分可以被重設(shè)。此外,一旦一個對象被凍結(jié),它不能被解凍,所以所有屬性在程序運行期間都保持只讀。
在我想允許重新賦值但是在表層限制的地方,閉包比起對象更方便和靈活。在我不想重新賦值的地方,一個凍結(jié)的對象比起重復(fù) const 聲明在我所有的函數(shù)中更方便一些。
許多函數(shù)式編程者在重新賦值上采取了一個強硬的立場:它不應(yīng)該被使用。他們傾向使用 const 來使用所有閉包變量只讀,并且他們使用 Ojbect.freeze(..) 或者完全不可變數(shù)據(jù)結(jié)構(gòu)來防止屬性被重新賦值。此外,他們盡量在每個可能的地方減少顯式地聲明的/追蹤的變量,更傾向于值傳遞 —— 函數(shù)鏈,作為參數(shù)被傳遞的 return 值,等等 —— 替代中間值存儲。
這本書是關(guān)于 JavaScript 中的輕量級函數(shù)式編程,這是一個我與核心函數(shù)式編程群體有分歧的情況。
我認(rèn)為變量重新賦值當(dāng)被合理的使用時是相當(dāng)有用的,它的明確性具有相當(dāng)有可讀性。從經(jīng)驗來看,在插入 debugger 或斷點或跟蹤表表達(dá)式時,調(diào)試工作要容易得多。
狀態(tài)拷貝正如我們在第 6 章學(xué)習(xí)的,防止副作用侵蝕代碼可預(yù)測性的最好方法之一是確保我們將所有狀態(tài)值視為不可變的,無論他們是否真的可變(凍結(jié))與否。
如果你沒有使用特別定制的庫來提供復(fù)雜的不可變數(shù)據(jù)結(jié)構(gòu),最簡單滿足要求的方法:在每次變化前復(fù)制你的對象或者數(shù)組。
數(shù)組淺拷貝很容易:只要使用 slice() 方法:
var a = [ 1, 2, 3 ]; var b = a.slice(); b.push( 4 ); a; // [1,2,3] b; // [1,2,3,4]
對象也可以相對容易地實現(xiàn)淺拷貝:
var o = { x: 1, y: 2 }; // 在 ES2017 以后,使用對象的解構(gòu): var p = { ...o }; p.y = 3; // 在 ES2015 以后: var p = Object.assign( {}, o ); p.y = 3;
如果對象或數(shù)組中的值是非基本類型(對象或數(shù)組),使用深拷貝你不得不手動遍歷每一層來拷貝每個內(nèi)嵌對象。否則,你將有這些內(nèi)部對象的共享引用拷貝,這就像給你的程序邏輯造成了一次大破壞。
你是否意識到克隆是可行的只是因為所有的這些狀態(tài)值是可見的并且可以如此簡單地被拷貝?一堆被包裝在閉包里的狀態(tài)會怎么樣,你如何拷貝這些狀態(tài)?
那是相當(dāng)乏味的。基本上,你不得不做一些類似之前我們自定義 forEach API 的方法:提供一個閉包內(nèi)層擁有提取或拷貝隱藏值權(quán)限的函數(shù),并在這過程中創(chuàng)建新的等價閉包。
盡管這在理論上是可行的,對讀者來說也是一種鍛煉!這個實現(xiàn)的操作量遠(yuǎn)遠(yuǎn)不及你可能進(jìn)行的任何真實程序的調(diào)整。
在表示需要拷貝的狀態(tài)時,對象具有一個更明顯的優(yōu)勢。
性能從實現(xiàn)的角度看,對象有一個比閉包有利的原因,那就是 JavaScript 對象通常在內(nèi)存和甚至計算角度是更加輕量的。
但是需要小心這個普遍的斷言:有很多東西可以用來處理對象,這會抹除你從無視閉包轉(zhuǎn)向?qū)ο鬆顟B(tài)追蹤獲得的任何性能增益。
讓我們考慮一個情景的兩種實現(xiàn)。首先,閉包方式實現(xiàn):
function StudentRecord(name,major,gpa) { return function printStudent(){ return `${name}, Major: ${major}, GPA: ${gpa.toFixed(1)}`; }; } var student = StudentRecord( "Kyle Simpson", "kyle@some.tld", "CS", 4 ); // 隨后 student(); // Kyle Simpson, Major: CS, GPA: 4.0
內(nèi)部函數(shù) printStudeng() 封裝了三個變量:name、major 和 gpa。它維護(hù)這個狀態(tài)無論我們是否傳遞引用給這個函數(shù),在這個例子我們稱它為 student()。
現(xiàn)在看對象(和 this)方式:
function StudentRecord(){ return `${this.name}, Major: ${this.major}, GPA: ${this.gpa.toFixed(1)}`; } var student = StudentRecord.bind( { name: "Kyle Simpson", major: "CS", gpa: 4 } ); // 隨后 student(); // Kyle Simpson, Major: CS, GPA: 4.0
student() 函數(shù),學(xué)術(shù)上叫做“邊界函數(shù)” —— 有一個硬性邊界 this 來引用我們傳入的對象字面量,因此之后任何調(diào)用 student() 將使用這個對象作為this,于是它的封裝狀態(tài)可以被訪問。
兩種實現(xiàn)有相同的輸出:一個保存狀態(tài)的函數(shù),但是關(guān)于性能,會有什么不同呢?
注意:精準(zhǔn)可控地判斷 JS 代碼片段性能是非常困難的事情。我們在這里不會深入所有的細(xì)節(jié),但是我強烈推薦你閱讀《你不知道的 JS:異步和性能》這本書,特別是第 6 章“性能測試和調(diào)優(yōu)”,來了解細(xì)節(jié)。
如果你寫過一個庫來創(chuàng)造持有配對狀態(tài)的函數(shù),要么在第一個片段中調(diào)用 studentRecord(..),要么在第二個片段中調(diào)用 StudentRecord.bind(..)的方式,你可能更多的關(guān)心它們兩的性能怎樣。檢查代碼,我們可以看到前者每次都必須創(chuàng)建一個新函數(shù)表達(dá)式。后者使用 bind(..),沒有明顯的含義。
思考 bind(..) 在內(nèi)部做了什么的一種方式是創(chuàng)建一個閉包來替代函數(shù),像這樣:
function bind(orinFn,thisObj) { return function boundFn(...args) { return origFn.apply( thisObj, args ); }; } var student = bind( StudentRecord, { name: "Kyle.." } );
這樣,看起來我們的場景的兩種實現(xiàn)都是創(chuàng)造一個閉包,所以性能看似也是一致的。
但是,內(nèi)置的 bind(..) 工具并不一定要創(chuàng)建閉包來完成任務(wù)。它只是簡單地創(chuàng)建了一個函數(shù),然后手動設(shè)置它的內(nèi)部 this 給一個指定的對象。這可能比起我們使用閉包本身是一個更高效的操作。
我們這里討論的在每次操作上的這種性能優(yōu)化是不值一提的。但是如果你的庫的關(guān)鍵部分被使用了成千上萬次甚至更多,那么節(jié)省的時間會很快增加。許多庫 —— Bluebird 就是這樣一個例子,它已經(jīng)完成移除閉包去使用對象的優(yōu)化。
在庫的使用案例之外,持有配對狀態(tài)的函數(shù)通常在應(yīng)用的關(guān)鍵路徑發(fā)生的次數(shù)相對非常少。相比之下,典型的使用是函數(shù)加狀態(tài) —— 在任意一個片段調(diào)用 student(),是更加常見的。
如果你的代碼中也有這樣的場景,你應(yīng)該更多地考慮(優(yōu)化)前后的性能對比。
歷史上的邊界函數(shù)通常具有一個相當(dāng)糟糕的性能,但是最近已經(jīng)被 JS 引擎高度優(yōu)化。如果你在幾年前檢測過這些變化,很可能跟你現(xiàn)在用最近的引擎重復(fù)測試的結(jié)果完全不一致。
邊界函數(shù)現(xiàn)在看起來至少跟同樣的封裝函數(shù)表現(xiàn)的一樣好。所以這是另一個支持對象比閉包好的點。
我只想重申:性能觀察結(jié)果不是絕對的,在一個給定場景下決定什么是最好的是非常復(fù)雜的。不要隨意使用你從別人那里聽到的或者是你從之前一些項目中看到的。小心的決定對象還是閉包更適合這個任務(wù)。
總結(jié)本章的真理無法被直述。必須閱讀本章來尋找它的真理。
【上一章】翻譯連載 | JavaScript輕量級函數(shù)式編程-第6章:值的不可變性 |《你不知道的JS》姊妹篇
iKcamp原創(chuàng)新書《移動Web前端高效開發(fā)實戰(zhàn)》已在亞馬遜、京東、當(dāng)當(dāng)開售。
>> 滬江Web前端上海團(tuán)隊招聘【W(wǎng)eb前端架構(gòu)師】,有意者簡歷至:zhouyao@hujiang.com <<
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/88524.html
摘要:我稱之為輕量級函數(shù)式編程。序眾所周知,我是一個函數(shù)式編程迷。函數(shù)式編程有很多種定義。本書是你開啟函數(shù)式編程旅途的絕佳起點。事實上,已經(jīng)有很多從頭到尾正確的方式介紹函數(shù)式編程的書了。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson - 《You-Dont-Know-JS》作者 譯者團(tuán)隊(排名不分先后):阿希、blueken、brucecham、...
摘要:本書主要探索函數(shù)式編程的核心思想。我們在中應(yīng)用的僅僅是一套基本的函數(shù)式編程概念的子集。我稱之為輕量級函數(shù)式編程。通常來說,關(guān)于函數(shù)式編程的書籍都熱衷于拓展閱讀者的知識面,并企圖覆蓋更多的知識點。,本書統(tǒng)稱為函數(shù)式編程者。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson - 《You-Dont-Know-JS》作者 譯者團(tuán)隊(排名不分先后)...
摘要:從某些方面來講,這章回顧的函數(shù)知識并不是針對函數(shù)式編程者,非函數(shù)式編程者同樣需要了解。什么是函數(shù)針對函數(shù)式編程,很自然而然的我會想到從函數(shù)開始。如果你計劃使用函數(shù)式編程,你應(yīng)該盡可能多地使用函數(shù),而不是程序。指的是一個函數(shù)聲明的形參數(shù)量。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson - 《You-Dont-Know-JS》作者 關(guān)于譯者:...
摘要:為此決定自研一個富文本編輯器。例如當(dāng)要轉(zhuǎn)化的對象有環(huán)存在時子節(jié)點屬性賦值了父節(jié)點的引用,為了關(guān)于函數(shù)式編程的思考作者李英杰,美團(tuán)金融前端團(tuán)隊成員。只有正確使用作用域,才能使用優(yōu)秀的設(shè)計模式,幫助你規(guī)避副作用。 JavaScript 專題之惰性函數(shù) JavaScript 專題系列第十五篇,講解惰性函數(shù) 需求 我們現(xiàn)在需要寫一個 foo 函數(shù),這個函數(shù)返回首次調(diào)用時的 Date 對象,注意...
摘要:把數(shù)據(jù)的流向想象成糖果工廠的一條傳送帶,每一次操作其實都是冷卻切割包裝糖果中的一步。在該章節(jié)中,我們將會用糖果工廠的類比來解釋什么是組合。糖果工廠靠這套流程運營的很成功,但是和所有的商業(yè)公司一樣,管理者們需要不停的尋找增長點。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個流淌...
閱讀 3968·2021-11-16 11:44
閱讀 5189·2021-10-09 09:54
閱讀 2031·2019-08-30 15:44
閱讀 1678·2019-08-29 17:22
閱讀 2753·2019-08-29 14:11
閱讀 3389·2019-08-26 13:25
閱讀 2324·2019-08-26 11:55
閱讀 1595·2019-08-26 10:37