感謝社區(qū)中各位的大力支持隧出,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券胀瞪,享受所有官網(wǎng)優(yōu)惠凄诞,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取
多年以前帆谍,Anton van Straaten 編寫(xiě)了一個(gè)名聲顯赫而且廣為流傳的 禪家公案既忆,描繪并挑起了閉包與對(duì)象之間一種重要的緊張狀態(tài)患雇。
莊嚴(yán)的 Qc Na 大師在與他的學(xué)生 Anton 一起散步。Anto 希望促成一次與師傅的討論酪术,他說(shuō):“師傅绘雁,我聽(tīng)說(shuō)對(duì)象是個(gè)非常好的東西 —— 真的嗎庐舟?” Qc Na 同情地看著他的學(xué)生回答道挪略,“笨學(xué)生 —— 對(duì)象只不過(guò)是一種簡(jiǎn)單的閉包杠娱√螅”
被訓(xùn)斥的 Anton 告別他的師父返回自己的房間室叉,開(kāi)始有意地學(xué)習(xí)閉包太惠。他仔細(xì)地閱讀了整部 “Lamda:終極……” 系列書(shū)籍以及其姊妹篇疲憋,并且使用一個(gè)基于閉包的對(duì)象系統(tǒng)實(shí)現(xiàn)了一個(gè)小的 Scheme 解釋器缚柳。他學(xué)到了很多秋忙,希望向他的師父報(bào)告自己的進(jìn)步灰追。
當(dāng)他再次與 Qc Na 散步時(shí)朴下,Anton 試圖給師傅一個(gè)好印象殴胧,說(shuō):“師父团滥,經(jīng)過(guò)勤奮的學(xué)習(xí)灸姊,現(xiàn)在我理解了對(duì)象確實(shí)是簡(jiǎn)化的閉包厨钻『话颍” Qc Na 用他的拐杖打了 Anton 作為回應(yīng)苍蔬,他說(shuō):“你到底什么時(shí)候才能明白碟绑?閉包只是簡(jiǎn)化的對(duì)象格仲】撸” 此時(shí)此刻侮东,Anton 茅塞頓開(kāi)悄雅。
Anton van Straaten 6/4/2003
http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg03277.html
原版的文章宽闲,雖然簡(jiǎn)短,但是擁有更多關(guān)于其起源與動(dòng)機(jī)的背景內(nèi)容娩梨,我強(qiáng)烈建議你閱讀這篇文章姚建,來(lái)為本章的學(xué)習(xí)正確地設(shè)置你的思維模式掸冤。
我見(jiàn)過(guò)許多讀過(guò)這段公案的人都對(duì)它的聰明機(jī)智表現(xiàn)出一絲假笑稿湿,然后并沒(méi)有改變太多他們的想法就離開(kāi)了饺藤。然而涕俗,一個(gè)公案的目的(從佛教禪的角度而言)就是刺激讀者對(duì)其中矛盾的真理上下求索再姑。所以元镀,回頭再讀一遍栖疑。然后再讀一遍遇革。
它到底是什么澳淑?閉包是簡(jiǎn)化的對(duì)象杠巡,或者對(duì)象是簡(jiǎn)化的閉包氢拥?或都不是嫩海?或都是叁怪?難道唯一的重點(diǎn)是閉包和對(duì)象在某種意義上是等價(jià)的奕谭?
而且這與函數(shù)式編程有什么關(guān)系血柳?拉一把椅子深思片刻难捌。如果你樂(lè)意的話根吁,這一章將是一次有趣的繞路遠(yuǎn)足击敌。
同一軌道
首先愚争,讓我們確保當(dāng)我們談到閉包和對(duì)象時(shí)我們都在同一軌道上轰枝。顯然我們的語(yǔ)境是 JavaScript 如何應(yīng)對(duì)這兩種機(jī)制鞍陨,而且具體來(lái)說(shuō)談到的是簡(jiǎn)單的函數(shù)閉包(參見(jiàn)第二章的“保持作用域”)與簡(jiǎn)單對(duì)象(鍵值對(duì)的集合)诚撵。
一個(gè)簡(jiǎn)單的函數(shù)閉包:
function outer() {
var one = 1;
var two = 2;
return function inner(){
return one + two;
};
}
var three = outer();
three(); // 3
一個(gè)簡(jiǎn)單的對(duì)象:
var obj = {
one: 1,
two: 2
};
function three(outer) {
return outer.one + outer.two;
}
three( obj ); // 3
當(dāng)你提到“閉包”時(shí)寿烟,很多人都會(huì)在腦中喚起許多額外的東西筛武,比如異步回調(diào),甚至是帶有封裝的模塊模式和信息隱藏榴都。相似地漠其,“對(duì)象”會(huì)把類帶到思維中和屎,this
眶俩、原型颠印、以及一大堆其他的工具和模式线罕。
隨著我們向前邁進(jìn)钞楼,我們將小心地解說(shuō)這種重要外部語(yǔ)境的一部分询件,但就目前來(lái)說(shuō)宛琅,只要抓住“閉包”與“對(duì)象”的最簡(jiǎn)單的解釋就好 —— 這將使我們的探索少一些困惑嘿辟。
看起來(lái)很像
閉包與對(duì)象是如何聯(lián)系在一起的红伦,這可能不太明顯昙读。所以讓我們首先來(lái)探索一下它們的相似性。
為了框定這次討論国撵,讓我簡(jiǎn)要地?cái)嘌詢杉拢?/p>
- 一個(gè)沒(méi)有閉包的編程語(yǔ)言可以使用對(duì)象來(lái)模擬閉包。
- 一個(gè)沒(méi)有對(duì)象的編程語(yǔ)言可以使用閉包來(lái)模擬對(duì)象壮虫。
換句話說(shuō)剩拢,我們可以認(rèn)為閉包和對(duì)象是同一種東西的兩種不同表現(xiàn)形式徐伐。
狀態(tài)
考慮這段從上面引用的代碼:
function outer() {
var one = 1;
var two = 2;
return function inner(){
return one + two;
};
}
var obj = {
one: 1,
two: 2
};
被 inner()
和對(duì)象 obj
封閉的兩個(gè)作用域都包含兩個(gè)狀態(tài)元素:帶有值 1
的 one
办素,和帶有值 2
的 two
性穿。在語(yǔ)法上和機(jī)制上需曾,這些狀態(tài)的表現(xiàn)形式是不同的呆万。而在概念上车份,它們其實(shí)十分相似躬充。
事實(shí)上充甚,將一個(gè)對(duì)象表示為一個(gè)閉包伴找,或者將一個(gè)閉包表示為一個(gè)對(duì)象是相當(dāng)直接了當(dāng)?shù)募及Hグ墒庵幔约涸囈幌拢?/p>
var point = {
x: 10,
y: 12,
z: 14
};
你有沒(méi)有想到過(guò)這樣的東西旁理?
function outer() {
var x = 10;
var y = 12;
var z = 14;
return function inner(){
return [x,y,z];
}
};
var point = outer();
注意: inner()
函數(shù)在每次被調(diào)用時(shí)創(chuàng)建并返回一個(gè)新數(shù)組(也就是一個(gè)對(duì)象!)夺艰。這是因?yàn)?JS 沒(méi)有給我們?nèi)魏?return
多個(gè)值的能力郁副,除非將它們封裝在一個(gè)對(duì)象中存谎。從技術(shù)上講,這并不違背我們的閉包做對(duì)象的任務(wù)草雕,因?yàn)檫@只是一個(gè)暴露/傳送值的實(shí)現(xiàn)細(xì)節(jié)墩虹;狀態(tài)追蹤本身依然是無(wú)對(duì)象的诫钓。使用 ES6+ 的數(shù)組解構(gòu)菌湃,我們可以在另一側(cè)聲明式地忽略這個(gè)臨時(shí)中間數(shù)組:var [x,y,z] = point()
惧所。從一個(gè)開(kāi)發(fā)者的人體工程學(xué)角度來(lái)說(shuō)下愈,這些值被分離地存儲(chǔ)而且是通過(guò)閉包而非對(duì)象追蹤的势似。
要是我們有一些嵌套的對(duì)象呢障簿?
var person = {
name: "Kyle Simpson",
address: {
street: "123 Easy St",
city: "JS'ville",
state: "ES"
}
};
我們可以使用嵌套的閉包來(lái)表示同種狀態(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();
讓我們實(shí)踐一下從另一個(gè)方向走站故,由閉包到對(duì)象:
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
世蔗,但我們可以將這些值作為一個(gè)對(duì)象明確地傳遞:
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)對(duì)象被明確地傳入,取代了隱含地持有這個(gè)狀態(tài)的閉包顶滩。
行為也是要出!
對(duì)象和閉包不僅代表表達(dá)狀態(tài)集合的方式,它們還可以通過(guò)函數(shù)/方法包含行為。將數(shù)據(jù)與它的行為打包有一個(gè)炫酷的名字:封裝析二。
考慮如下代碼:
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
叶摄,所以其中的功能和狀態(tài)一起保留了下來(lái)蛤吓。
我們可以使用 this
與一個(gè)對(duì)象的綁定取得相同的能力:
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)匀皇褂?happyBirthday()
函數(shù)來(lái)表達(dá)狀態(tài)數(shù)據(jù)的封裝锅棕,但使用一個(gè)對(duì)象而不是閉包淌山。而且我們不必向一個(gè)函數(shù)明確地傳入一個(gè)對(duì)象(比如前一個(gè)例子)顺少;JavaScript 的 this
綁定很容易地創(chuàng)建了一個(gè)隱含綁定脆炎。
另一種分析這種關(guān)系的方式是:一個(gè)閉包將一個(gè)函數(shù)與一組狀態(tài)聯(lián)系起來(lái)秒裕,而一個(gè)持有相同狀態(tài)的對(duì)象可以有任意多個(gè)操作這些狀態(tài)的函數(shù)几蜻。
事實(shí)上梭稚,你甚至可以使用一個(gè)閉包作為接口暴露多個(gè)方法弧烤∠景海考慮一個(gè)帶有兩個(gè)方法的傳統(tǒng)對(duì)象:
var person = {
firstName: "Kyle",
lastName: "Simpson",
first() {
return this.firstName;
},
last()
return this.lastName;
}
}
person.first() + " " + person.last();
// Kyle Simpson
僅使用閉包而非對(duì)象急波,我們可以將這個(gè)程序表示為:
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
雖然這些程序在人體工程學(xué)上的觀感不同名段,但它們實(shí)際上只是相同程序行為的不同種類實(shí)現(xiàn)吉嫩。
(不)可變性
許多人一開(kāi)始認(rèn)為閉包和對(duì)象在可變性方面表現(xiàn)不同自娩;閉包可以防止外部改變而對(duì)象不能忙迁。但是碎乃,事實(shí)表明梅誓,兩種形式具有完全相同的可變性行為。
這是因?yàn)槲覀冴P(guān)心的嗅回,正如第六章中所討論的绵载,是 值 的可變性苛白,而它是值本身的性質(zhì)娃豹,與它在哪里以及如何被賦值無(wú)關(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]
};
存儲(chǔ)在 outer()
內(nèi)部的詞法變量 x
中的值是不可變的 —— 記住购裙,2
這樣的基本類型根據(jù)定義就是不可變的懂版。但是被 y
引用的值,一個(gè)數(shù)組缓窜,絕對(duì)是可變的定续。這對(duì) xyPublic
上的屬性 x
和 y
來(lái)說(shuō)是完全一樣的。
我們可以佐證對(duì)象與閉包和不可變性無(wú)關(guān):指出 y
本身就是一個(gè)數(shù)組恩掷,如此我們需要將這個(gè)例子進(jìn)一步分解:
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
}
};
如果你將它考慮為 “烏龜(也就是對(duì)象)背地球”克滴,那么在最底下一層誓焦,所有的狀態(tài)數(shù)據(jù)都是基本類型,而所有的基本類型都是不可變的。
不管你是用嵌套的對(duì)象表示狀態(tài)越平,還是用嵌套的閉包表示狀態(tài)帽驯,被持有的值都是不可變的。
同構(gòu)
如今 “同構(gòu)” 這個(gè)詞經(jīng)常被扔到 JavaScript 旁邊,它通常用來(lái)指代可以在服務(wù)器與瀏覽器中使用/共享的代碼度气。一段時(shí)間以前我寫(xiě)過(guò)一篇博客,聲稱對(duì) “同構(gòu)” 這一詞的這種用法是一種捏造,它實(shí)際上有一種明確和重要的含義被掩蓋了比然。
同構(gòu)意味著什么?好吧,我們可以從數(shù)學(xué)上蓖墅,或社會(huì)學(xué)上,或生物學(xué)上討論它。同構(gòu)的一般概念是寥袭,你有兩個(gè)東西杰扫,它們雖然不同但在結(jié)構(gòu)上有相似之處。
在所有這些用法中凡伊,同構(gòu)與等價(jià)以這樣的方式被區(qū)分開(kāi):如果兩個(gè)值在所有的方面都完全相等惠豺,那么它們就是等價(jià)的。但如果它們表現(xiàn)不同苍苞,卻仍然擁有 1 對(duì) 1 的疗琉、雙向的映射關(guān)系,那么它們就是同構(gòu)的柠贤。
換言之餐弱,如果你能夠從 A 映射(轉(zhuǎn)換)到 B 而后又可以從用反向的映射從 B 走回到 A畸写,那么 A 和 B 就是同構(gòu)的。
回憶一下第二章的 “數(shù)學(xué)簡(jiǎn)憶”,我們討論了函數(shù)的數(shù)學(xué)定義 —— 輸入與輸出之間的映射真慢。我們指出這在技術(shù)上被稱為一種態(tài)射。同構(gòu)是雙射(也就是兩個(gè)方向的)的一種特殊情況,它不僅要求映射必須能夠在兩個(gè)方向上進(jìn)行,而且要求這兩種形式在行為上也完全一樣忆家。
把對(duì)數(shù)字的思考放在一邊胳搞,讓我們將同構(gòu)聯(lián)系到代碼上。再次引用我的博客:
如果 JS 中存在同構(gòu)這樣的東西,它將會(huì)是什么樣子阴绢?好吧,它可能是這樣:你擁有這樣一套 JS 代碼廉侧,它可以被轉(zhuǎn)換為另一套 JS 代碼连舍,而且(重要的是)如果你想這么做的話,你可將后者轉(zhuǎn)換回前者。
正如我們?cè)缦仁褂瞄]包即對(duì)象與對(duì)象即閉包的例子所主張的,這些表現(xiàn)形式可以從兩個(gè)方向轉(zhuǎn)換。以這種角度來(lái)說(shuō),它們互相是同構(gòu)的。
簡(jiǎn)而言之,閉包和對(duì)象是狀態(tài)(以及與之關(guān)聯(lián)的功能)的同構(gòu)表現(xiàn)形式。
當(dāng)一下次你聽(tīng)到某些人說(shuō) “X 與 Y 是同構(gòu)的”吼畏,那么他們的意思是性雄,“X 和 Y 可以在兩個(gè)方向上從一者轉(zhuǎn)換為另一者诀拭,并保持相同的行為。”
底層
那么,從我們可以編寫(xiě)的代碼的角度講农尖,我們可以認(rèn)為對(duì)象是閉包的一種同構(gòu)表現(xiàn)形式滑沧。但我們還可以發(fā)現(xiàn)令漂,一個(gè)閉包系統(tǒng)實(shí)際上可能 —— 而且很可能 —— 用對(duì)象來(lái)實(shí)現(xiàn)荚孵!
這樣考慮一下:在下面的代碼中滔驾,JS 如何在 outer()
已經(jīng)運(yùn)行過(guò)后摊阀,為了 inner()
保持變量 x
的引用而追蹤它跃捣?
function outer() {
var x = 1;
return function inner(){
return x;
};
}
我們可以想象娶聘,outer()
的作用域 —— 所有變量被定義的集合 —— 是用一個(gè)帶有屬性的對(duì)象實(shí)現(xiàn)的狡耻。那么,從概念上將瘫证,在內(nèi)存的某處,有這樣一些東西:
scopeOfOuter = {
x: 1
};
然后對(duì)于函數(shù) inner()
來(lái)說(shuō)坑赡,在它被創(chuàng)建時(shí)螟加,它得到一個(gè)稱為 scopeOfInner
的(空的)作用域?qū)ο笫蛲迹@個(gè)作用域?qū)ο笸ㄟ^(guò)它的 [[Prototype]]
鏈接到 scopeOfOuter
對(duì)象上恰起,有些像這樣:
scopeOfInner = {};
Object.setPrototypeOf( scopeOfInner, scopeOfOuter );
然后吨枉,在 inner()
內(nèi)部圃庭,當(dāng)它引用詞法變量 x
時(shí)书在,實(shí)際上更像是這樣:
return scopeOfInner.x;
scopeOfInner
沒(méi)有屬性 x
栈源,但它 [[Prototype]]
鏈接著擁有屬性 x
的 scopeOfOuter
。通過(guò)原型委托訪問(wèn) scopeOfOuter.x
的結(jié)果,于是值 1
被返回了。
以這種方式,我們可以看到為什么 outer()
即使是在運(yùn)行完成之后它的作用域也會(huì)被(通過(guò)閉包)保留下來(lái):因?yàn)閷?duì)象 scopeOfInner
鏈接著對(duì)象 scopeOfOuter
骇钦,因此這可以使這個(gè)對(duì)象和它的屬性完整地保留鳞仙。
這都是概念上的。我沒(méi)說(shuō) JS 引擎使用了對(duì)象和原型。但這 可以 相似地工作是完全說(shuō)得通的积糯。
許多語(yǔ)言確實(shí)是通過(guò)對(duì)象實(shí)現(xiàn)閉包的川慌。而另一些語(yǔ)言以閉包的形式實(shí)現(xiàn)對(duì)象琴拧。但至于它們?nèi)绾喂ぷ髋嫔牛覀冞€是讓讀者發(fā)揮他們的想象力吧八毯。
分道揚(yáng)鑣
那么閉包和對(duì)象是等價(jià)的泊交,對(duì)吧?不完全是雹熬。我打賭它們要比你在讀這一章之前看起來(lái)相似多了阵幸,但它們依然有重要的不同之處咬腕。
這些不同不應(yīng)視為弱點(diǎn)或用法上的爭(zhēng)議;那是錯(cuò)誤的視角火鼻。它們應(yīng)當(dāng)被視為使其中一者比另一者具有更適于(而且更合理!)某種特定任務(wù)的特性或優(yōu)勢(shì)。
結(jié)構(gòu)可變性
從概念上講,一個(gè)閉包的結(jié)構(gòu)是不可變的找筝。
換言之,你絕不可能向一個(gè)閉包添加或移除狀態(tài)胞锰。閉包是一種變量被聲明的位置(在編寫(xiě)/編譯時(shí)固定)的性質(zhì)兼雄,而且對(duì)任何運(yùn)行時(shí)條件都不敏感 —— 當(dāng)然佃乘,這假定你使用 strict 模式而且/或者沒(méi)有使用 eval(..)
這樣的東西作弊千扶!
注意: JS 引擎在技術(shù)上可以加工一個(gè)閉包來(lái)剔除任何在它作用域中的不再被使用的變量,但這對(duì)于開(kāi)發(fā)者來(lái)說(shuō)是一個(gè)透明的高級(jí)優(yōu)化图焰。無(wú)論引擎實(shí)際上是否會(huì)做這些種類的優(yōu)化,我想對(duì)于開(kāi)發(fā)者來(lái)說(shuō)最安全的做法是假定閉包是以作用域?yàn)閱挝坏挠拘悖且宰兞繛閱挝坏摹H绻悴幌胱屗媪粝聛?lái),就不要閉包它拍冠!
然而典蜕,對(duì)象默認(rèn)是相當(dāng)可變的轩缤。只要這個(gè)對(duì)象還沒(méi)有被凍結(jié)(Object.freeze(..)
)踊淳,你就可以自由地向一個(gè)對(duì)象添加或移除(delete
)屬性/下標(biāo)垄开。
能夠根據(jù)程序中運(yùn)行時(shí)的條件來(lái)追蹤更多(或更少)的狀態(tài)净捅,可能是代碼的一種優(yōu)勢(shì)豆村。
例如撬统,讓我們想象一個(gè)游戲中對(duì)擊鍵事件的追蹤。幾乎可以肯定蚀狰,你想要使用一個(gè)數(shù)組來(lái)這樣做:
function trackEvent(evt,keypresses = []) {
return keypresses.concat( evt );
}
var keypresses = trackEvent( newEvent1 );
keypresses = trackEvent( newEvent2, keypresses );
注意: 你有沒(méi)有發(fā)現(xiàn)专肪,為什么我使用 concat(..)
而不是直接向 keypresses
中 push(..)
?因?yàn)樵?FP 中势篡,我們總是想將數(shù)組視為一種不可變 —— 可以被重新創(chuàng)建并添加新元素 —— 的數(shù)據(jù)結(jié)構(gòu)隶糕,而不是直接被改變的疤估。我們用了一個(gè)明確的重新復(fù)制將副作用的惡果替換掉了(稍后有更多關(guān)于這一點(diǎn)的內(nèi)容)缠俺。
雖然我們沒(méi)有改變數(shù)組的結(jié)構(gòu)盒使,但如果我們想的話就可以辑畦。待會(huì)兒會(huì)詳細(xì)說(shuō)明這一點(diǎn)乖杠。
但數(shù)組并不是追蹤不斷增長(zhǎng)的 evt
對(duì)象 “列表” 的唯一方式肾砂。我們可以使用閉包:
function trackEvent(evt,keypresses = () => []) {
return function newKeypresses() {
return [ ...keypresses(), evt ];
};
}
var keypresses = trackEvent( newEvent1 );
keypresses = trackEvent( newEvent2, keypresses );
你發(fā)現(xiàn)這里發(fā)生了什么嗎?
每當(dāng)我們向 “列表” 中添加一個(gè)新事件即硼,我們就在既存的 keypresses()
函數(shù)(閉包) —— 她持有當(dāng)前的 evt
對(duì)象 —— 周圍創(chuàng)建了一個(gè)新的閉包僻澎。當(dāng)我們調(diào)用 keypresses()
函數(shù)時(shí),它將依次調(diào)用所有嵌套著的函數(shù),建立起一個(gè)所有分別被閉包的 evt
對(duì)象的中間數(shù)組冒黑。同樣拼苍,閉包是追蹤所有這些狀態(tài)的機(jī)制立莉;你看到的數(shù)組只是為了從一個(gè)函數(shù)中返回多個(gè)值而出現(xiàn)的一個(gè)實(shí)現(xiàn)細(xì)節(jié)。
那么哪一個(gè)適合我們的任務(wù)?不出意料地查近,數(shù)組的方式可能要合適得多次哈。閉包在結(jié)構(gòu)上的不可變性意味著我們唯一的選擇是在它之上包裹更多的閉包趾撵。對(duì)象默認(rèn)就是可擴(kuò)展的取试,所我們只要按需要加長(zhǎng)數(shù)組即可位迂。
順帶一提元莫,雖然我將這種結(jié)構(gòu)上的(不)可變性作為閉包和對(duì)象間的一種明顯的不同茎截,但是我們將對(duì)象作為一個(gè)不可變的值來(lái)使用的方式實(shí)際上更加像是一種相似性招刨。
為每次數(shù)組的遞增創(chuàng)建一個(gè)新數(shù)組(通過(guò) concat(..)
)就是講數(shù)組視為結(jié)構(gòu)上不可變的蹬叭,這與閉包是結(jié)構(gòu)上不可變的設(shè)計(jì)初衷在概念上是平行的丐一。
私有性
在分析閉包 vs 對(duì)象時(shí)藻糖,你可能想到的第一個(gè)不同就是閉包通過(guò)嵌套的詞法作用域提供了狀態(tài)的“私有性”,而對(duì)象將所有的東西都作為公共屬性暴露出來(lái)库车。這樣的私有性有一個(gè)炫酷的名字:信息隱藏巨柒。
考慮一下詞法閉包隱藏:
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
對(duì)于一般的軟件工程原理來(lái)說(shuō)這里有一些明顯的不同 —— 考慮到抽象,帶有公共和私有 API 的模塊模式柠衍,等等 —— 但是讓我們將我們的討論限定在 FP 的角度之上洋满;畢竟,這是一本關(guān)于函數(shù)式編程的書(shū)珍坊!
可見(jiàn)性
隱藏信息的能力看起來(lái)似乎是一種人們渴望的狀態(tài)追蹤的特性牺勾,但是我相信 FP 程序員們可能會(huì)持反對(duì)意見(jiàn)。
將狀態(tài)作為一個(gè)對(duì)象上的公共屬性進(jìn)行管理的一個(gè)好處是阵漏,枚舉(并迭代Wっ瘛)狀態(tài)中所有的數(shù)據(jù)更簡(jiǎn)單。想象你想要處理每一個(gè)擊鍵事件(早先的一個(gè)例子)來(lái)將它存入數(shù)據(jù)庫(kù)履怯,使用這樣一個(gè)工具:
function recordKeypress(keypressEvt) {
// 數(shù)據(jù)庫(kù)工具
DB.store( "keypress-events", keypressEvt );
}
如果你已經(jīng)擁有了一個(gè)數(shù)組 —— 一個(gè)帶有數(shù)字命名屬性的對(duì)象 —— 那么使用一個(gè) JS 內(nèi)建的數(shù)組工具 forEach(..)
完成這個(gè)任務(wù)就非常直接了當(dāng):
keypresses.forEach( recordKeypress );
但是回还,如果擊鍵的列表被隱藏在閉包中的話,你就不得不在閉包的公共 API 上暴露一個(gè)工具叹洲,并使它擁有訪問(wèn)隱藏?cái)?shù)據(jù)的特權(quán)柠硕。
例如,我們可以給閉包的 keypresses
示例一個(gè)它自己的 forEach
运提,就像數(shù)組擁有的內(nèi)建函數(shù)一樣:
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 );
一對(duì)象的狀態(tài)數(shù)據(jù)的可見(jiàn)性使得它使用起來(lái)更直接蝗柔,而閉包隱晦的狀態(tài)使我們不得不做更多的工作來(lái)處理它闻葵。
改變控制
如果詞法變量 x
隱藏在一個(gè)閉包中,那么唯一能夠?qū)λM(jìn)行重新賦值的代碼也一定在這個(gè)閉包中癣丧;從外部修改 x
是不可能的笙隙。
正如我們?cè)诘诹轮锌吹降模瑑H這一點(diǎn)就改善了代碼的可讀性坎缭,它減小了讀者為了判定一個(gè)已知變量的行為而必須考慮的代碼的表面積。
詞法上重新賦值的局部接近性是我不覺(jué)得 const
是一個(gè)有用特性的一大原因签钩。作用域(因此閉包也是)一般來(lái)說(shuō)應(yīng)當(dāng)都很小掏呼,這意味著僅有幾行代碼可能會(huì)影響到重新賦值。在上面的 outer()
中铅檩,我們可以很快地檢視并看到?jīng)]有代碼對(duì) x
進(jìn)行重新賦值憎夷,所以對(duì)于一切目的和意圖來(lái)說(shuō)它都是一個(gè)常數(shù)。
這種保證及大地增強(qiáng)了我們?cè)诤瘮?shù)的純粹性上的信心昧旨。
另一方面拾给,xPublic.x
是一個(gè)公共屬性,程序中任何得到 xPublic
引用的部分都默認(rèn)地有能力將 xPublic.x
重新賦值為其他的某些值兔沃。要考慮的代碼行數(shù)可要多多了蒋得!
這就是為什么在第六章中,我們看到 Object.freeze(..)
以一種簡(jiǎn)單粗暴的方式將一個(gè)對(duì)象的所有屬性都設(shè)置為只讀(writable: false
)乒疏,這樣一來(lái)它們就不會(huì)不可預(yù)知地被重新賦值了额衙。
不幸的是,Object.freeze(..)
會(huì)凍結(jié)所有屬性而且不可逆轉(zhuǎn)怕吴。
使用閉包窍侧,你讓一些代碼擁有改變的特權(quán),而程序的其余部分依然受限转绷。但你凍結(jié)一個(gè)對(duì)象時(shí)伟件,程序中沒(méi)有任何部分能夠進(jìn)行重新賦值。另外议经,一旦一個(gè)對(duì)象被凍結(jié)斧账,它就不能再被解凍,于是它的屬性會(huì)在程序運(yùn)行期間一直保持只讀狀態(tài)煞肾。
在那些我想允許重新賦值但限制它影響范圍的地方其骄,閉包就是一種比對(duì)象更加方便而且靈活的方式。在我想要禁止重新賦值的地方扯旷,一個(gè)凍結(jié)的對(duì)象要比在我的函數(shù)中到處重復(fù) const
聲明方便多了拯爽。
許多 FP 程序員對(duì)重新賦值采取了強(qiáng)硬的立場(chǎng):它就不應(yīng)當(dāng)被使用。他們傾向于使用 const
將所有閉包變量都成為只讀钧忽,而且他們使用 Object.freeze(..)
或者完全不可變的數(shù)據(jù)結(jié)構(gòu)來(lái)防止屬性被重新賦值毯炮。另外逼肯,他們還會(huì)盡可能地減少被明確聲明/追蹤的屬性的數(shù)量,使用值的傳送 —— 函數(shù)鏈桃煎,將 return
值作為參數(shù)傳遞篮幢,等等 —— 來(lái)取代值的臨時(shí)存儲(chǔ)。
這本書(shū)講的是 JavaScript 的“輕量函數(shù)式”編程为迈,而這就是我與 FP 的核心人群意見(jiàn)相左的情況之一三椿。
我認(rèn)為變量的重新賦值可以十分有用,而且葫辐,如果使用得當(dāng)搜锰,它的明確性相當(dāng)易讀。而且從經(jīng)驗(yàn)上講耿战,當(dāng)你在調(diào)試中插入 debugger
或斷點(diǎn)蛋叼,或者一個(gè)監(jiān)視表達(dá)式的時(shí)候,將會(huì)更容易剂陡。
狀態(tài)克隆
正如我們?cè)诘诹轮袑W(xué)到的狈涮,防止副作用損害我們代碼的可預(yù)見(jiàn)性的最佳方法之一,就是確保我們將所有狀態(tài)值都視為不可變的鸭栖,而不管它們實(shí)際上是否真的是不可變(被凍結(jié))的歌馍。
如果你沒(méi)有在使用一個(gè)專門(mén)為此建造的、提供了精巧的不可變數(shù)據(jù)結(jié)構(gòu)的庫(kù)晕鹊,那么最簡(jiǎn)單的方法也夠了:在每次改變你的對(duì)象/數(shù)組之前復(fù)制它們骆姐。
數(shù)組很容易淺克隆:使用 slice()
方法就行:
var a = [ 1, 2, 3 ];
var b = a.slice();
b.push( 4 );
a; // [1,2,3]
b; // [1,2,3,4]
對(duì)象也可以相對(duì)容易地進(jìn)行淺克履筇狻:
var o = {
x: 1,
y: 2
};
// 在 ES2017+ 中玻褪,使用對(duì)象擴(kuò)散操作:
var p = { ...o };
p.y = 3;
// 在 ES2015+ 中:
var p = Object.assign( {}, o );
p.y = 3;
如果在一個(gè)對(duì)象/數(shù)組中的值本身就是非基本類型(對(duì)象/數(shù)組),那么為了進(jìn)行深度克隆公荧,你就必須手動(dòng)遍歷并克隆每一個(gè)被嵌套的對(duì)象带射。否則,你會(huì)得到那些字對(duì)象的共享引用的拷貝循狰,而這很可能會(huì)在你的程序邏輯中造成災(zāi)難窟社。
你有沒(méi)有注意到,這種克隆之所以可能绪钥,僅僅是由于所有這些狀態(tài)值都可見(jiàn)灿里,并因此可以很容易拷貝?那么包裝在一個(gè)閉包中的一組狀態(tài)呢程腹?你如何拷貝那些狀態(tài)匣吊?
那可麻煩多了。事實(shí)上,你不得不做一些與我們之前自定義的 forEach
API 方法相似的事情:在閉包的每一層內(nèi)部都提供一個(gè)有權(quán)抽取/拷貝隱藏值的函數(shù)色鸳,一路創(chuàng)建新的等價(jià)閉包社痛。
即便這在理論上是可能的 —— 給讀者的另一個(gè)練習(xí)! —— 但與你對(duì)任何真實(shí)的程序所作出的可能的調(diào)整相比命雀,它也遠(yuǎn)不切實(shí)際蒜哀。
當(dāng)對(duì)象用來(lái)表示我們想要克隆的狀態(tài)時(shí),它具有明顯的好處吏砂。
性能
一個(gè)對(duì)象可能優(yōu)于閉包的原因撵儿,從實(shí)現(xiàn)的角度講,是在 JavaScript 中對(duì)象在內(nèi)存甚至計(jì)算的意義上更輕量狐血。
但將之作為一個(gè)一般性的結(jié)論要小心:在你無(wú)視閉包并轉(zhuǎn)向基于對(duì)象的狀態(tài)追蹤時(shí)可能會(huì)得到一些性能的增益淀歇,但在你能對(duì)對(duì)象所做的事情中,有相當(dāng)一部分可以抹除這些增益氛雪。
讓我們使用兩種實(shí)現(xiàn)考慮同一個(gè)場(chǎng)景。首先耸成,閉包風(fēng)格的實(shí)現(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ù) printStudent()
閉包著三個(gè)變量 name
报亩、major
、和 gpa
井氢。無(wú)論我們?cè)诤翁巶魉鸵粋€(gè)指向這個(gè)函數(shù)的引用弦追,它都會(huì)維護(hù)這個(gè)狀態(tài) —— 在這個(gè)例子中我們稱之為 student()
。
現(xiàn)在輪到對(duì)象(和 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ù) —— 技術(shù)上成為一個(gè)“被綁定函數(shù)” —— 擁有一個(gè)硬綁定的 this
引用花竞,它指向我們傳入的對(duì)象字面量劲件,這樣稍后對(duì) student()
的調(diào)用將會(huì)使用這個(gè)對(duì)象作為 this
,因此可以訪問(wèn)它所封裝的狀態(tài)约急。
這兩種實(shí)現(xiàn)都有相同的結(jié)果:一個(gè)保留著狀態(tài)的函數(shù)零远。那么性能呢?會(huì)有什么不同厌蔽?
注意: 準(zhǔn)確地牵辣、可操作地判斷一段 JS 代碼的性能是一件非常棘手的事情。我們不會(huì)在此深入所有的細(xì)節(jié)奴饮,但我強(qiáng)烈建議你閱讀“你不懂 JS:異步與性能”一書(shū)纬向,特別是第六章“基準(zhǔn)分析與調(diào)優(yōu)”,來(lái)了解更多細(xì)節(jié)戴卜。
如果你在編寫(xiě)一個(gè)庫(kù)逾条,它創(chuàng)建一個(gè)帶有函數(shù)的狀態(tài) —— 要么是一個(gè)代碼段中對(duì) StudentRecord(..)
的調(diào)用,要么是第二個(gè)代碼段中對(duì) StudentRecord.bind(..)
的調(diào)用 —— 你最關(guān)心的很可能是它們兩個(gè)如何工作投剥。檢視它們的代碼师脂,我們可以發(fā)現(xiàn)前者不得不每次創(chuàng)建一個(gè)新的函數(shù)表達(dá)式。而第二個(gè)使用了 bind(..)
,這由于它的隱晦而不那么明顯危彩。
考慮 bind(..)
在底層如何工作的一種方式是攒磨,它在函數(shù)之上創(chuàng)建了一個(gè)閉包,就像這樣:
function bind(orinFn,thisObj) {
return function boundFn(...args) {
return origFn.apply( thisObj, args );
};
}
var student = bind( StudentRecord, { name: "Kyle.." } );
以這種方式汤徽,看起來(lái)我們這種場(chǎng)景的兩種實(shí)現(xiàn)都創(chuàng)建了閉包娩缰,因此它們的性能很可能是相同的。
然而谒府,內(nèi)建的 bind(..)
工具不必真的創(chuàng)建閉包來(lái)完成這個(gè)任務(wù)拼坎。它只是創(chuàng)建一個(gè)函數(shù)并手動(dòng)地將它內(nèi)部的 this
設(shè)置為指定的對(duì)象。這潛在地是一種比我們自己做的閉包更高效的操作完疫。
我們?cè)谶@里討論的這種性能提升在個(gè)別的操作中的影響微乎其微泰鸡。但如果你的庫(kù)的關(guān)鍵路徑在成百上千次,或更多地重復(fù)這件事壳鹤,那么這種提升的效果就會(huì)很快累加起來(lái)盛龄。許多的庫(kù) —— 例如 Bluebird 就是一例 —— 都正是由于這個(gè)原因,最終通過(guò)移除閉包而使用對(duì)象來(lái)進(jìn)行了優(yōu)化芳誓。
在庫(kù)之外的用例當(dāng)中余舶,帶有自己函數(shù)的狀態(tài)通常只在一個(gè)應(yīng)用程序的關(guān)鍵路徑上相對(duì)少地出現(xiàn)幾次。對(duì)比之下锹淌,函數(shù) + 狀態(tài)的用法 —— 在兩個(gè)代碼段中對(duì) student()
的調(diào)用 —— 通常更常見(jiàn)匿值。
如果這正是你代碼中的某些已知情況,那么你可能應(yīng)當(dāng)更多地關(guān)心后者的性能與前者的對(duì)比赂摆。
長(zhǎng)久以來(lái)被綁定的函數(shù)的性能通常都很爛挟憔,但是最近它已經(jīng)被 JS 引擎進(jìn)行了相當(dāng)高度的優(yōu)化。如果你在幾年前曾經(jīng)對(duì)這些種類的函數(shù)進(jìn)行過(guò)基準(zhǔn)分析烟号,那么你在最新的引擎上重復(fù)相同的測(cè)試的話绊谭,就完全有可能得到不同的結(jié)果。
如今汪拥,一個(gè)被綁定函數(shù)性能最差也能與它閉包函數(shù)的等價(jià)物相同龙誊。所以這是另一個(gè)首選對(duì)象而非閉包的理由。
我想要重申:這些性能上的觀測(cè)不是絕對(duì)的喷楣,而且對(duì)于一個(gè)已知場(chǎng)景判定什么對(duì)它最合適是非常復(fù)雜的趟大。不要只是隨便地使用一些道聽(tīng)途說(shuō)的,或者你曾將在以前的項(xiàng)目中見(jiàn)過(guò)的東西铣焊。要仔細(xì)地檢查對(duì)象或閉包是否能恰當(dāng)逊朽、高效地完成你當(dāng)前的任務(wù)。
總結(jié)
這一章的真理是無(wú)法付諸筆頭的曲伊。你必須閱讀這一章來(lái)找出它的真理叽讳。