特別說明摸屠,為便于查閱芍殖,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS
如果說本書后半部分(第四到六章)有什么關(guān)鍵信息茁瘦,那就是類是一種代碼的可選設(shè)計模式(不是必要的)床未,而且用像 JavaScript 這樣的 [[Prototype]]
語言來實現(xiàn)它總是很尷尬屈尼。
雖然這種尷尬很大一部分關(guān)于語法伯顶,但 不僅 限于此李滴。第四和第五章審視了相當(dāng)多的難看語法觉义,從使代碼雜亂的 .prototype
引用的繁冗设拟,到 顯式假想多態(tài):當(dāng)你在鏈條的不同層級上給方法相同的命名以試圖實現(xiàn)從低層方法到高層方法的多態(tài)引用慨仿。.constructor
被錯誤地解釋為“被XX構(gòu)建”鸽扁,這成為了一個不可靠的定義,也成為了另一個難看的語法镶骗。
但關(guān)于類的設(shè)計的問題要深刻多了桶现。第四章指出在傳統(tǒng)的面向類語言中,類實際上發(fā)生了從父類向子類鼎姊,由子類向?qū)嵗?拷貝 動作骡和,而在 [[Prototype]]
中,動作 不是 一個拷貝相寇,而是相反 —— 一個委托鏈接慰于。
OLOO 風(fēng)格和行為委托接受了 [[Prototype]]
,而不是將它隱藏起來唤衫,當(dāng)比較它們的簡單性時婆赠,類在 JS 中的問題就凸顯出來。
class
我們 不必 再次爭論這些問題佳励。我在這里簡單地重提這些問題僅僅是為了使它們在你的頭腦里保持新鮮休里,以使我們將注意力轉(zhuǎn)向 ES6 的 class
機制。我們將在這里展示它如何工作赃承,并且看看 class
是否實質(zhì)上解決了任何這些“類”的問題妙黍。
讓我們重溫第六章的 Widget
/Button
例子:
class Widget {
constructor(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
}
class Button extends Widget {
constructor(width,height,label) {
super( width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
render($where) {
super.render( $where );
this.$elem.click( this.onClick.bind( this ) );
}
onClick(evt) {
console.log( "Button '" + this.label + "' clicked!" );
}
}
除了語法上 看起來 更好,ES6 還解決了什么瞧剖?
- 不再有(某種意義上的拭嫁,繼續(xù)往下看!)指向
.prototype
的引用來弄亂代碼抓于。 -
Button
被聲明為直接“繼承自”(也就是extends
)Widget
做粤,而不是需要用Object.create(..)
來替換.prototype
鏈接的對象,或者用__proto__
和Object.setPrototypeOf(..)
來設(shè)置它捉撮。 -
super(..)
現(xiàn)在給了我們非常有用的 相對多態(tài) 的能力怕品,所以在鏈條上某一個層級上的任何方法,可以引用鏈條上相對上一層的同名方法呕缭。第四章中有一個關(guān)于構(gòu)造器的奇怪現(xiàn)象:構(gòu)造器不屬于它們的類堵泽,而且因此與類沒有聯(lián)系。super(..)
含有一個對此問題的解決方法 ——super()
會在構(gòu)造器內(nèi)部想如你期望的那樣工作恢总。 -
class
字面語法對指定屬性沒有什么啟發(fā)(僅對方法有)迎罗。這看起來限制了某些東西,但是絕大多數(shù)情況下期望一個屬性(狀態(tài))存在于鏈條末端的“實例”以外的地方片仿,這通常是一個錯誤和令人詫異(因為這個狀態(tài)被隱含地在所有“實例”中“分享”)的纹安。所以,也可以說class
語法防止你出現(xiàn)錯誤。 -
extends
甚至允許你用非常自然的方式擴展內(nèi)建的對象(子)類型厢岂,比如Array
或者RegExp
光督。在沒有class .. extends
的情況下這樣做一直以來是一個極端復(fù)雜而令人沮喪的任務(wù),只有最熟練的框架作者曾經(jīng)正確地解決過這個問題∷#現(xiàn)在结借,它是小菜一碟!
憑心而論卒茬,對大多數(shù)明顯的(語法上的)問題船老,和經(jīng)典的原型風(fēng)格代碼使人詫異的地方,這些確實是實質(zhì)上的解決方案圃酵。
class
的坑
然而柳畔,它不全是優(yōu)點。在 JS 中將“類”作為一種設(shè)計模式郭赐,仍然有一些深刻和非常令人煩惱的問題薪韩。
首先,class
語法可能會說服你 JS 在 ES6 中存在一個新的“類”機制捌锭。但不是這樣俘陷。 class
很大程度上僅僅是一個既存的 [[Prototype]]
(委托)機制的語法糖!
這意味著 class
實際上不是像傳統(tǒng)面向類的語言那樣舀锨,在聲明時靜態(tài)地拷貝定義岭洲。如果你在“父類”上更改/替換了一個方法(有意或無意地),子“類”和/或?qū)嵗龑艿健坝绊憽笨材洌驗樗鼈冊诼暶鲿r沒有得到一份拷貝,它們依然都使用那個基于 [[Prototype]]
的實時委托模型雷激。
class C {
constructor() {
this.num = Math.random();
}
rand() {
console.log( "Random: " + this.num );
}
}
var c1 = new C();
c1.rand(); // "Random: 0.4324299..."
C.prototype.rand = function() {
console.log( "Random: " + Math.round( this.num * 1000 ));
};
var c2 = new C();
c2.rand(); // "Random: 867"
c1.rand(); // "Random: 432" -- oops!!!
這種行為只有在 你已經(jīng)知道了 關(guān)于委托的性質(zhì)替蔬,而不是期待從“真的類”中 拷貝 時,才看起來合理屎暇。那么你要問自己的問題是承桥,為什么你為了根本上就和類不同的東西選擇 class
語法?
ES6 的 class
語法不是使觀察和理解傳統(tǒng)的類和委托對象間的不同 變得更困難 了嗎根悼?
class
語法 沒有 提供聲明類的屬性成員的方法(僅對方法有)凶异。所以如果你需要跟蹤對象間分享的狀態(tài),那么你最終會回到難看的 .prototype
語法挤巡,像這樣:
class C {
constructor() {
// 確保修改的是共享狀態(tài)
// 不是設(shè)置實例上的遮蔽屬性
C.prototype.count++;
// 這里剩彬,`this.count` 通過委托如我們期望的那樣工作
console.log( "Hello: " + this.count );
}
}
// 直接在原型對象上添加一個共享屬性
C.prototype.count = 0;
var c1 = new C();
// Hello: 1
var c2 = new C();
// Hello: 2
c1.count === 2; // true
c1.count === c2.count; // true
這里最大的問題是,由于它將 .prototype
作為實現(xiàn)細節(jié)暴露(泄露?蟊啊)出來喉恋,而背叛了 class
語法的初衷。
而且,我們還依然面臨著那個令人詫異的陷阱:this.count++
將會隱含地在 c1
和 c2
兩個對象上創(chuàng)建一個分離的遮蔽屬性 .count
轻黑,而不是更新共享的狀態(tài)糊肤。class
沒有在這個問題上給我們什么安慰,除了(大概是)通過缺少語法支持來暗示你 根本 就不應(yīng)該這么做氓鄙。
另外馆揉,無意地遮蔽依然是個災(zāi)難:
class C {
constructor(id) {
// 噢,一個坑抖拦,我們用實例上的屬性值遮蔽了`id()`方法
this.id = id;
}
id() {
console.log( "Id: " + this.id );
}
}
var c1 = new C( "c1" );
c1.id(); // TypeError -- `c1.id` 現(xiàn)在是字符串"c1"
還有一些關(guān)于 super
如何工作的微妙問題升酣。你可能會假設(shè) super
將會以一種類似與 this
得到綁定的方式(間第二章)來被綁定,也就是 super
總是會綁定到當(dāng)前方法在 [[Prototype]]
鏈中的位置的更高一層蟋座。
然而拗踢,因為性能問題(this
綁定已經(jīng)很耗費性能了),super
不是動態(tài)綁定的向臀。它在聲明時巢墅,被有些“靜態(tài)地”綁定。不是什么大事兒券膀,對吧君纫?
恩…… 可能是,可能不是芹彬。如果你像大多數(shù) JS 開發(fā)者那樣蓄髓,開始把函數(shù)以各種不同的方式賦值給不同的(來自于 class
定義的)對象,你可能不會意識到在所有這些情況下舒帮,底層的 super
機制會不得不每次都重新綁定会喝。
而且根據(jù)你每次賦值采取的語法方式不同,很有可能在某些情況下 super
不能被正確地綁定(至少不會像你期望的那樣)玩郊,所以你可能(在寫作這里時肢执,TC39 正在討論這個問題)會不得不用 toMethod(..)
來手動綁定 super
(有點兒像你不得不用 bind(..)
綁定 this
—— 見第二章)。
你曾經(jīng)可以給不同的對象賦予方法译红,來通過 隱含綁定 規(guī)則(見第二章)预茄,自動地利用 this
的動態(tài)性。但對于使用 super
的方法侦厚,同樣的事情很可能不會發(fā)生耻陕。
考慮這里 super
應(yīng)當(dāng)怎樣動作(對 D
和 E
):
class P {
foo() { console.log( "P.foo" ); }
}
class C extends P {
foo() {
super();
}
}
var c1 = new C();
c1.foo(); // "P.foo"
var D = {
foo: function() { console.log( "D.foo" ); }
};
var E = {
foo: C.prototype.foo
};
// E 鏈接到 D 來進行委托
Object.setPrototypeOf( E, D );
E.foo(); // "P.foo"
如果你(十分合理地!)認為 super
將會在調(diào)用時自動綁定刨沦,你可能會期望 super()
將會自動地認識到 E
委托至 D
诗宣,所以使用 super()
的 E.foo()
應(yīng)當(dāng)調(diào)用 D.foo()
。
不是這樣已卷。 由于實用主義的性能原因梧田,super
不像 this
那樣 延遲綁定(也就是動態(tài)綁定)淳蔼。相反它從調(diào)用時 [[HomeObject]].[[Prototype]]
派生出來,而 [[HomeObject]]
是在聲明時靜態(tài)綁定的裁眯。
在這個特定的例子中鹉梨,super()
依然解析為 P.foo()
,因為方法的 [[HomeObject]]
仍然是 C
而且 C.[[Prototype]]
是 P
穿稳。
可能 會有方法手動地解決這樣的陷阱存皂。在這個場景中使用 toMethod(..)
來綁定/重綁定方法的 [[HomeObject]]
(設(shè)置這個對象的 [[Prototype]]
一起!)似乎會管用:
var D = {
foo: function() { console.log( "D.foo" ); }
};
// E 鏈接到 D 來進行委托
var E = Object.create( D );
// 手動綁定 `foo` 的 `[[HomeObject]]` 到
// `E`, 因為 `E.[[Prototype]]` 是 `D`逢艘,所以
// `super()` 是 `D.foo()`
E.foo = C.prototype.foo.toMethod( E, "foo" );
E.foo(); // "D.foo"
注意: toMethod()
克隆這個方法旦袋,然后將它的第一個參數(shù)作為 homeObject
(這就是為什么我們傳入 E
),第二個參數(shù)(可選)用來設(shè)置新方法的 name
(保持“foo”不變)它改。
除了這種場景以外疤孕,是否還有其他的極端情況會使開發(fā)者們落入陷阱還有待觀察。無論如何央拖,你將不得不費心保持清醒:在哪里引擎自動為你確定 super
祭阀,和在哪里你不得不手動處理它。噢鲜戒!
靜態(tài)優(yōu)于動態(tài)专控?
但是關(guān)于 ES6 的最大問題是,所有這些種種陷阱意味著 class
有點兒將你帶入一種語法遏餐,它看起來暗示著(像傳統(tǒng)的類那樣)一旦你聲明一個 class
伦腐,它是一個東西的靜態(tài)定義(將來會實例化)。使你完全忘記了這個事實:C
是一個對象失都,一個你可以直接互動的具體的東西柏蘑。
在傳統(tǒng)面向類的語言中,你從不會在晚些時候調(diào)整類的定義粹庞,所以類設(shè)計模式不提供這樣的能力辩越。但是 JS 的 一個最強大的部分 就是它 是 動態(tài)的,而且任何對象的定義都是(除非你將它設(shè)定為不可變)不固定的可變的 東西信粮。
class
看起來在暗示你不應(yīng)該做這樣的事情,通過強制你使用 .prototype
語法才能做到趁啸,或強制你考慮 super
的陷阱强缘,等等。而且它對這種動態(tài)機制可能帶來的一切陷阱 幾乎不 提供任何支持不傅。
換句話說旅掂,class
好像在告訴你:“動態(tài)太壞了,所以這可能不是一個好主意访娶。這里有看似靜態(tài)語法商虐,把你的東西靜態(tài)編碼。”
關(guān)于 JavaScript 的評論是多么悲傷懊爻怠:動態(tài)太難了典勇,讓我們假裝成(但實際上不是!)靜態(tài)吧叮趴。
這些就是為什么 ES6 的 class
偽裝成一個語法頭痛癥的解決方案割笙,但是它實際上把水?dāng)嚨酶鼫啠腋蝗菀讓?JS 形成清晰簡明的認識眯亦。
注意: 如果你使用 .bind(..)
工具制作一個硬綁定函數(shù)(見第二章)伤溉,那么這個函數(shù)是不能像普通函數(shù)那樣用 ES6 的 extend
擴展的。
復(fù)習(xí)
class
在假裝修復(fù) JS 中的類/繼承設(shè)計模式的問題上做的很好妻率。但它實際上做的卻正相反:它隱藏了許多問題乱顾,而且引入了其他微妙而且危險的東西。
class
為折磨了 JavaScript 語言將近二十年的“類”的困擾做出了新的貢獻宫静。在某些方面走净,它問的問題比它解決的多,而且在 [[Prototype]]
機制的優(yōu)雅和簡單之上囊嘉,它整體上感覺像是一個非常不自然的匹配温技。
底線:如果 ES6 class
使穩(wěn)健地利用 [[Prototype]]
變得困難,而且隱藏了 JS 對象機制最重要的性質(zhì) —— 對象間的實時委托鏈接 —— 我們不應(yīng)該認為 class
產(chǎn)生的麻煩比它解決的更多扭粱,并且將它貶低為一種反模式嗎舵鳞?
我真的不能幫你回答這個問題。但我希望這本書已經(jīng)在你從未經(jīng)歷過的深度上完全地探索了這個問題琢蛤,而且已經(jīng)給出了 你自己回答這個問題 所需的信息蜓堕。