感謝社區(qū)中各位的大力支持,譯者再次奉上一點點福利:阿里云產品券,享受所有官網優(yōu)惠突委,并抽取幸運大獎:點擊這里領取
如果說本書后半部分(第四到六章)有什么關鍵信息昆著,那就是類是一種代碼的可選設計模式(不是必要的),而且用像JavaScript這樣的[[Prototype]]
語言來實現(xiàn)它總是很尷尬缸夹。
雖然這種尷尬很大一部分關于語法痪寻,但 不僅 限于此螺句。第四和第五章審視了相當多的難看語法,從使代碼雜亂的.prototype
引用的繁冗橡类,到 顯式假想多態(tài):當你在鏈條的不同層級上給方法相同的命名以試圖實現(xiàn)從低層方法到高層方法的多態(tài)引用蛇尚。.constructor
被錯誤地解釋為“被XX構建”,這成為了一個不可靠的定義顾画,也成為了另一個難看的語法取劫。
但關于類的設計的問題要深刻多了。第四章指出在傳統(tǒng)的面向類語言中研侣,類實際上發(fā)生了從父類向子類谱邪,由子類向實例的 拷貝 動作,而在[[Prototype]]
中庶诡,動作 不是 一個拷貝惦银,而是相反——一個委托鏈接。
OLOO風格和行為委托接受了[[Prototype]]
灌砖,而不是將它隱藏起來璧函,當比較它們的簡單性時,類在JS中的問題就凸顯出來基显。
class
我們 不必 再次爭論這些問題蘸吓。我在這里簡單地重提這些問題僅僅是為了使它們在你的頭腦里保持新鮮,以使我們將注意力轉向ES6的class
機制撩幽。我們將在這里展示它如何工作库继,并且看看class
是否實質上解決了任何這些“類”的問題。
讓我們重溫第六章的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ù)往下看Uザ琛)指向
.prototype
的引用來弄亂代碼拜英。 -
Button
被聲明為直接“繼承自”(也就是extends
)Widget
,而不是需要用Object.create(..)
來替換.prototype
鏈接的對象琅催,或者用__proto__
和Object.setPrototypeOf(..)
來設置它居凶。 -
super(..)
現(xiàn)在給了我們非常有用的 相對多態(tài) 的能力,所以在鏈條上某一個層級上的任何方法藤抡,可以引用鏈條上相對上一層的同名方法侠碧。第四章中有一個關于構造器的奇怪現(xiàn)象:構造器不屬于它們的類,而且因此與類沒有聯(lián)系缠黍。super(..)
含有一個對此問題的解決方法 ——super()
會在構造器內部想正如你期望的那樣工作弄兜。 -
class
字面語法對指定屬性沒有什么啟發(fā)(僅對方法有)。這看起來限制了某些東西,但是絕大多數情況下期望一個屬性(狀態(tài))存在于鏈條末端的“實例”以外的地方替饿,這通常是一個錯誤和令人詫異(因為這個狀態(tài)被隱含地在所有“實例”中“分享”)的语泽。所以,也可以說class
語法防止你出現(xiàn)錯誤盛垦。 -
extends
甚至允許你用非常自然的方式擴展內建的對象(子)類型湿弦,比如Array
或者RegExp
。在沒有class .. extends
的情況下這樣做一直以來是一個極端復雜而令人沮喪的任務腾夯,只有最熟練的框架作者曾經正確地解決過這個問題〖瞻#現(xiàn)在,它是小菜一碟蝶俱!
憑心而論班利,對大多數明顯的(語法上的)問題,和經典的原型風格代碼使人詫異的地方榨呆,這些確實是實質上的解決方案罗标。
class
的坑
然而,它不全是優(yōu)點积蜻。在JS中將“類”作為一種設計模式闯割,仍然有一些深刻和非常令人煩惱的問題。
首先竿拆,class
語法可能會說服你JS在ES6中存在一個新的“類”機制宙拉。但不是這樣。class
很大程度上僅僅是一個既存的[[Prototype]]
(委托)機制的語法糖丙笋!
這意味著class
實際上不是像傳統(tǒng)面向類語言那樣谢澈,在聲明時靜態(tài)地拷貝定義。如果你在“父類”上更改/替換了一個方法(有意或無意地)御板,子“類”和/或實例將會受到“影響”锥忿,因為它們在聲明時沒有得到一份拷貝,它們依然都使用那個基于[[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!!!
這種行為只有在 你已經知道了 關于委托的性質敬鬓,而不是期待從“真的類”中 拷貝 時,才看起來合理笙各。那么你要問自己的問題是钉答,為什么你為了根本上就和類不同的東西選擇class
語法?
ES6的class
語法不是使觀察和理解傳統(tǒng)的類和委托對象間的不同 變得更困難 了嗎酪惭?
class
語法 沒有 提供聲明類的屬性成員的方法(僅對方法有)希痴。所以如果你需要跟蹤對象間分享的狀態(tài)者甲,那么你最終會回到丑陋的.prototype
語法春感,像這樣:
class C {
constructor() {
// 確保修改的是共享狀態(tài)
// 不是設置實例上的遮蔽屬性
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é)暴露(泄露v昀痢)出來嫩实,而背叛了class
語法的初衷。
而且窥岩,我們還依然面臨著那個令人詫異的陷阱:this.count++
將會隱含地在c1
和c2
兩個對象上創(chuàng)建一個分離的遮蔽屬性.count
甲献,而不是更新共享的狀態(tài)。class
沒有在這個問題上給我們什么安慰颂翼,除了(大概是)通過缺少語法支持來暗示你 根本 就不應該這么做晃洒。
另外,無意地遮蔽依然是個災難:
class C {
constructor(id) {
// 噢朦乏,一個坑球及,我們用實例上的屬性值遮蔽了`id()`方法
this.id = id;
}
id() {
console.log( "Id: " + id );
}
}
var c1 = new C( "c1" );
c1.id(); // TypeError -- `c1.id` 現(xiàn)在是字符串"c1"
還有一些關于super
如何工作的微妙問題。你可能會假設super
將會以一種類似與this
得到綁定的方式(間第二章)來被綁定呻疹,也就是super
總是會綁定到當前方法在[[Prototype]]
鏈中的位置的更高一層吃引。
然而,因為性能問題(this
綁定已經很耗費性能了)刽锤,super
不是動態(tài)綁定的。它在聲明時,被有些“靜態(tài)地”綁定鹿寨。不是什么大事兒瞧柔,對吧?
恩……可能是纺荧,可能不是旭愧。如果你像大多數JS開發(fā)者那樣,開始把函數賦值給不同的(來自于class
定義的)對象宙暇,以各種不同的方式输枯,你可能不會意識到在所有這些情況下,底層的super
機制會不得不每次都重新綁定占贫。
而且根據你每次賦值采取的語法方式不同桃熄,很有可能在某些情況下super
不能被正確地綁定(至少不會像你期望的那樣),所以你可能(在寫作這里時型奥,TC39正在討論這個問題)會不得不用toMethod(..)
來手動綁定super
(有點兒像你不得不用bind(..)
綁定this
—— 見第二章)瞳收。
你曾經可以給不同的對象賦予方法,來通過 隱含綁定 規(guī)則(見第二章)厢汹,自動地利用this
的動態(tài)性螟深。但對于使用super
的方法,同樣的事情很可能不會發(fā)生烫葬。
考慮這里super
應當怎樣動作(對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
將會在調用時自動綁定凡蜻,你可能會期望super()
將會自動地認識到E
委托至D
,所以使用super()
的E.foo()
應當調用D.foo()
垢箕。
不是這樣划栓。 由于實用主義的性能原因,super
不像this
那樣 延遲綁定(也就是動態(tài)綁定)条获。相反它從調用時[[HomeObject]].[[Prototype]]
派生出來忠荞,而[[HomeObject]]
實在聲明時靜態(tài)綁定的。
在這個特定的例子中帅掘,super()
依然解析為P.foo()
委煤,因為方法的[[HomeObject]]
仍然是C
而且C.[[Prototype]]
是P
。
可能 會有方法手動地解決這樣的陷阱修档。在這個場景中使用toMethod(..)
來綁定/重綁定方法的[[HomeObject]]
(設置這個對象的[[Prototype]]
一起K乇辍)似乎會管用:
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”不變)。
除了這種場景以外癣诱,是否還有其他的極端情況會使開發(fā)者們陷入陷阱還有待觀察计维。無論如何,你將不得不費心保持清醒:在哪里引擎自動為你確定super
撕予,和在哪里你不得不手動處理它鲫惶。噢!
靜態(tài)優(yōu)于動態(tài)实抡?
但是關于ES6的最大問題是欠母,所有這些種種陷阱意味著class
有點兒將你帶入一種語法,它看起來暗示著(像傳統(tǒng)的類那樣)一旦你聲明一個class
吆寨,它是一個東西的靜態(tài)定義(將來會實例化)赏淌。使你完全忘記了這個事實:C
是一個對象,一個你可以直接互動的具體的東西啄清。
在傳統(tǒng)面向類的語言中六水,你從不會在晚些時候調整類的定義,所以類設計模式不提供這樣的能力辣卒。但是JS的 一個最強大的部分 就是它 是 動態(tài)的掷贾,而且任何對象的定義都是(除非你將它設定為不可變)不固定的可變的 東西。
class
看起來在暗示你不應該做這樣的事情荣茫,通過強制你使用.prototype
語法才能做到想帅,或強制你考慮super
的陷阱,等等啡莉。而且它對這種動態(tài)機制可能帶來的一切陷阱 幾乎不 提供任何支持港准。
換句話說憎乙,class
好像在告訴你:“動態(tài)太壞了,所以這可能不是一個好主意叉趣。這里有看似靜態(tài)語法,把你的東西靜態(tài)編碼该押×粕迹”
關于JavaScript的評論是多么悲傷啊:動態(tài)太難了蚕礼,讓我們假裝成(但實際上不是Q叹摺)靜態(tài)吧。
這些就是為什么ES6的class
偽裝成一個語法頭痛癥的解決方案奠蹬,但是它實際上把水攪得更渾朝聋,而且更不容易對JS形成清晰簡明的認識。
注意: 如果你使用.bind(..)
工具制作一個硬綁定函數(見第二章)囤躁,那么這個函數是不能像普通函數那樣用ES6的extend
擴展的冀痕。
復習
class
在假裝修復JS中的類/繼承設計模式的問題上做的很好。但他實際上做的卻正相反:它隱藏了許多問題狸演,而且引入了其他微妙而且危險的東西言蛇。
class
為折磨了JavaScript語言將近20年的“類”的困擾做出了新的貢獻。在某些方面宵距,它問的問題比它解決的多腊尚,而且在[[Prototype]]
機制的優(yōu)雅和簡單之上,它整體上感覺像是一個非常不自然的匹配满哪。
底線:如果ES6class
使穩(wěn)健地利用[[Prototype]]
變得困難婿斥,而且隱藏了JS對象機制最重要的性質 —— 對象間的實時委托鏈接 —— 我們不應該認為class
產生的麻煩比它解決的更多,并且將它貶低為一種反模式嗎哨鸭?
我真的不能幫你回答這個問題民宿。但我希望這本書已經在你從未經歷過的深度上完全地探索了這個問題,而且已經給出了 你自己回答這個問題 所需的信息像鸡。