# 附錄A: ES6?`class`
如果說本書后半部分(第四到六章)有什么關鍵信息,那就是類是一種代碼的可選設計模式(不是必要的),而且用像 JavaScript 這樣的?`[[Prototype]]`?語言來實現它總是很尷尬。
雖然這種尷尬很大一部分關于語法,但?*不僅*?限于此。第四和第五章審視了相當多的難看語法,從使代碼雜亂的?`.prototype`?引用的繁冗,到?*顯式假想多態*:當你在鏈條的不同層級上給方法相同的命名以試圖實現從低層方法到高層方法的多態引用。`.constructor`?被錯誤地解釋為“被XX構建”,這成為了一個不可靠的定義,也成為了另一個難看的語法。
但關于類的設計的問題要深刻多了。第四章指出在傳統的面向類語言中,類實際上發生了從父類向子類,由子類向實例的?*拷貝*動作,而在?`[[Prototype]]`?中,動作?不是?一個拷貝,而是相反 —— 一個委托鏈接。
OLOO 風格和行為委托接受了?`[[Prototype]]`,而不是將它隱藏起來,當比較它們的簡單性時,類在 JS 中的問題就凸顯出來。
## `class`
我們?*不必*?再次爭論這些問題。我在這里簡單地重提這些問題僅僅是為了使它們在你的頭腦里保持新鮮,以使我們將注意力轉向 ES6 的?`class`?機制。我們將在這里展示它如何工作,并且看看?`class`?是否實質上解決了任何這些“類”的問題。
讓我們重溫第六章的?`Widget`/`Button`?例子:
```source-js
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 還解決了什么?
1. 不再有(某種意義上的,繼續往下看!)指向?`.prototype`?的引用來弄亂代碼。
2. `Button`?被聲明為直接“繼承自”(也就是?`extends`)`Widget`,而不是需要用?`Object.create(..)`?來替換?`.prototype`?鏈接的對象,或者用?`__proto__`?和?`Object.setPrototypeOf(..)`?來設置它。
3. `super(..)`?現在給了我們非常有用的?相對多態?的能力,所以在鏈條上某一個層級上的任何方法,可以引用鏈條上相對上一層的同名方法。第四章中有一個關于構造器的奇怪現象:構造器不屬于它們的類,而且因此與類沒有聯系。`super(..)`含有一個對此問題的解決方法 ——?`super()`?會在構造器內部想如你期望的那樣工作。
4. `class`?字面語法對指定屬性沒有什么啟發(僅對方法有)。這看起來限制了某些東西,但是絕大多數情況下期望一個屬性(狀態)存在于鏈條末端的“實例”以外的地方,這通常是一個錯誤和令人詫異(因為這個狀態被隱含地在所有“實例”中“分享”)的。所以,也可以說?`class`?語法防止你出現錯誤。
5. `extends`?甚至允許你用非常自然的方式擴展內建的對象(子)類型,比如?`Array`?或者?`RegExp`。在沒有?`class .. extends`?的情況下這樣做一直以來是一個極端復雜而令人沮喪的任務,只有最熟練的框架作者曾經正確地解決過這個問題。現在,它是小菜一碟!
憑心而論,對大多數明顯的(語法上的)問題,和經典的原型風格代碼使人詫異的地方,這些確實是實質上的解決方案。
## `class`?的坑
然而,它不全是優點。在 JS 中將“類”作為一種設計模式,仍然有一些深刻和非常令人煩惱的問題。
首先,`class`?語法可能會說服你 JS 在 ES6 中存在一個新的“類”機制。但不是這樣。?`class`?很大程度上僅僅是一個既存的?`[[Prototype]]`(委托)機制的語法糖!
這意味著?`class`?實際上不是像傳統面向類的語言那樣,在聲明時靜態地拷貝定義。如果你在“父類”上更改/替換了一個方法(有意或無意地),子“類”和/或實例將會受到“影響”,因為它們在聲明時沒有得到一份拷貝,它們依然都使用那個基于?`[[Prototype]]`?的實時委托模型。
```source-js
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!!!
```
這種行為只有在?*你已經知道了*?關于委托的性質,而不是期待從“真的類”中?*拷貝*?時,才看起來合理。那么你要問自己的問題是,為什么你為了根本上就和類不同的東西選擇?`class`?語法?
ES6 的?`class`?語法不是使觀察和理解傳統的類和委托對象間的不同?變得更困難?了嗎?
`class`?語法?*沒有*?提供聲明類的屬性成員的方法(僅對方法有)。所以如果你需要跟蹤對象間分享的狀態,那么你最終會回到難看的?`.prototype`?語法,像這樣:
```source-js
class C {
constructor() {
// 確保修改的是共享狀態
// 不是設置實例上的遮蔽屬性
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`?作為實現細節暴露(泄露!)出來,而背叛了?`class`?語法的初衷。
而且,我們還依然面臨著那個令人詫異的陷阱:`this.count++`?將會隱含地在?`c1`?和?`c2`?兩個對象上創建一個分離的遮蔽屬性?`.count`,而不是更新共享的狀態。`class`?沒有在這個問題上給我們什么安慰,除了(大概是)通過缺少語法支持來暗示你?*根本*?就不應該這么做。
另外,無意地遮蔽依然是個災難:
```source-js
class C {
constructor(id) {
// 噢,一個坑,我們用實例上的屬性值遮蔽了`id()`方法
this.id = id;
}
id() {
console.log( "Id: " + this.id );
}
}
var c1 = new C( "c1" );
c1.id(); // TypeError -- `c1.id` 現在是字符串"c1"
```
還有一些關于?`super`?如何工作的微妙問題。你可能會假設?`super`?將會以一種類似與?`this`?得到綁定的方式(間第二章)來被綁定,也就是?`super`?總是會綁定到當前方法在?`[[Prototype]]`?鏈中的位置的更高一層。
然而,因為性能問題(`this`?綁定已經很耗費性能了),`super`?不是動態綁定的。它在聲明時,被有些“靜態地”綁定。不是什么大事兒,對吧?
恩…… 可能是,可能不是。如果你像大多數 JS 開發者那樣,開始把函數以各種不同的方式賦值給不同的(來自于?`class`?定義的)對象,你可能不會意識到在所有這些情況下,底層的?`super`?機制會不得不每次都重新綁定。
而且根據你每次賦值采取的語法方式不同,很有可能在某些情況下?`super`?不能被正確地綁定(至少不會像你期望的那樣),所以你可能(在寫作這里時,TC39 正在討論這個問題)會不得不用?`toMethod(..)`?來手動綁定?`super`(有點兒像你不得不用?`bind(..)`?綁定?`this`?—— 見第二章)。
你曾經可以給不同的對象賦予方法,來通過?*隱含綁定*?規則(見第二章),自動地利用?`this`?的動態性。但對于使用?`super`?的方法,同樣的事情很可能不會發生。
考慮這里?`super`?應當怎樣動作(對?`D`?和?`E`):
```source-js
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`?將會在調用時自動綁定,你可能會期望?`super()`?將會自動地認識到?`E`?委托至?`D`,所以使用?`super()`?的?`E.foo()`?應當調用?`D.foo()`。
不是這樣。?由于實用主義的性能原因,`super`?不像?`this`?那樣?*延遲綁定*(也就是動態綁定)。相反它從調用時?`[[HomeObject]].[[Prototype]]`?派生出來,而?`[[HomeObject]]`?是在聲明時靜態綁定的。
在這個特定的例子中,`super()`?依然解析為?`P.foo()`,因為方法的?`[[HomeObject]]`?仍然是?`C`?而且?`C.[[Prototype]]`?是?`P`。
*可能*?會有方法手動地解決這樣的陷阱。在這個場景中使用?`toMethod(..)`?來綁定/重綁定方法的?`[[HomeObject]]`(設置這個對象的?`[[Prototype]]`?一起!)似乎會管用:
```source-js
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()`?克隆這個方法,然后將它的第一個參數作為?`homeObject`(這就是為什么我們傳入?`E`),第二個參數(可選)用來設置新方法的?`name`(保持“foo”不變)。
除了這種場景以外,是否還有其他的極端情況會使開發者們落入陷阱還有待觀察。無論如何,你將不得不費心保持清醒:在哪里引擎自動為你確定?`super`,和在哪里你不得不手動處理它。噢!
# 靜態優于動態?
但是關于 ES6 的最大問題是,所有這些種種陷阱意味著?`class`?有點兒將你帶入一種語法,它看起來暗示著(像傳統的類那樣)一旦你聲明一個?`class`,它是一個東西的靜態定義(將來會實例化)。使你完全忘記了這個事實:`C`?是一個對象,一個你可以直接互動的具體的東西。
在傳統面向類的語言中,你從不會在晚些時候調整類的定義,所以類設計模式不提供這樣的能力。但是 JS 的?一個最強大的部分?就是它?*是*?動態的,而且任何對象的定義都是(除非你將它設定為不可變)不固定的可變的?*東西*。
`class`?看起來在暗示你不應該做這樣的事情,通過強制你使用?`.prototype`?語法才能做到,或強制你考慮?`super`?的陷阱,等等。而且它對這種動態機制可能帶來的一切陷阱?*幾乎不*?提供任何支持。
換句話說,`class`?好像在告訴你:“動態太壞了,所以這可能不是一個好主意。這里有看似靜態語法,把你的東西靜態編碼。”
關于 JavaScript 的評論是多么悲傷啊:動態太難了,讓我們假裝成(但實際上不是!)靜態吧。
這些就是為什么 ES6 的?`class`?偽裝成一個語法頭痛癥的解決方案,但是它實際上把水攪得更渾,而且更不容易對 JS 形成清晰簡明的認識。
注意:?如果你使用?`.bind(..)`?工具制作一個硬綁定函數(見第二章),那么這個函數是不能像普通函數那樣用 ES6 的?`extend`?擴展的。
## 復習
`class`?在假裝修復 JS 中的類/繼承設計模式的問題上做的很好。但它實際上做的卻正相反:它隱藏了許多問題,而且引入了其他微妙而且危險的東西。
`class`?為折磨了 JavaScript 語言將近二十年的“類”的困擾做出了新的貢獻。在某些方面,它問的問題比它解決的多,而且在?`[[Prototype]]`?機制的優雅和簡單之上,它整體上感覺像是一個非常不自然的匹配。
底線:如果 ES6?`class`?使穩健地利用?`[[Prototype]]`?變得困難,而且隱藏了 JS 對象機制最重要的性質 ——?對象間的實時委托鏈接?—— 我們不應該認為?`class`?產生的麻煩比它解決的更多,并且將它貶低為一種反模式嗎?
我真的不能幫你回答這個問題。但我希望這本書已經在你從未經歷過的深度上完全地探索了這個問題,而且已經給出了?*你自己回答這個問題*?所需的信息。