天天學(xué)前端 第二部分 ES6 中的 Class
附錄 A ES6 中的 Class
可以用一句話總結(jié)本書的第二部分(第 4 章至第 6 章):類是一種可選(而不是必須)的設(shè)計(jì)模式,而且在 JavaScript 這樣的[[Prototype]]語言中實(shí)現(xiàn)類是很別扭的。
這種別扭的感覺不只是來源于語法, 雖然語法是很重要的原因。第 4 章和第 5 章介紹了許多語法的缺點(diǎn):繁瑣雜亂的.prototype 引用、 試圖調(diào)用原型鏈上層同名函數(shù)時(shí)的顯式偽多態(tài)(參見第 4 章)以及不可靠、不美觀而且容易被誤解成“構(gòu)造函數(shù)”的.constructor。
除此之外, 類設(shè)計(jì)其實(shí)還存在更深刻的問題。第 4 章指出, 傳統(tǒng)面向類的語言中父類和子類、 子類和實(shí)例之間其實(shí)是復(fù)制操作, 但是在[[Prototype]]中并沒有復(fù)制, 相反, 它們之間只有委托關(guān)聯(lián)。
對(duì)象關(guān)聯(lián)代碼和行為委托(參見第 6 章)使用了[[Prototype]]而不是將它藏起來, 對(duì)比其簡潔性可以看出,類并不適用于 JavaScript。
A.1 class
不過我們并不需要再糾結(jié)于這個(gè)問題,這里提到只是讓你簡單回憶一下;現(xiàn)在我們來看看 ES6 的 class 機(jī)制。我們會(huì)介紹它的工作原理并分析 class 是否改進(jìn)了之前提到的那些缺點(diǎn)。
首先回顧一下第 6 章中的 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 = $(").text(this.label);
}
render($where) {
super($where);
this.$elem.click(this.onClick.bind(this));
}
onClick(evt) {
console.log("Button '" + this.label + "' clicked!");
}
}
除了語法更好看之外,ES6 還解決了什么問題呢?
(基本上,下面會(huì)詳細(xì)介紹)不再引用雜亂的.prototype 了。
Button 聲明時(shí)直接“繼承”了 Widget,不再需要通過 Object.create(..)來替換.prototype 對(duì)象,也不需要設(shè)置.proto 或者 Object.setPrototypeOf(..)。
可以通過 super(..)來實(shí)現(xiàn)相對(duì)多態(tài), 這樣任何方法都可以引用原型鏈上層的同名方法。這可以解決第 4 章提到過的那個(gè)問題:構(gòu)造函數(shù)不屬于類, 所以無法互相引用——super()可以完美解決構(gòu)造函數(shù)的問題。
class 字面語法不能聲明屬性(只能聲明方法)??雌饋磉@是一種限制, 但是它會(huì)排除掉許多不好的情況, 如果沒有這種限制的話, 原型鏈末端的“實(shí)例”可能會(huì)意外地獲取其他地方的屬性(這些屬性隱式被所有“實(shí)例”所“共享”)。所以,class 語法實(shí)際上可以幫助你避免犯錯(cuò)。
可以通過 extends 很自然地?cái)U(kuò)展對(duì)象(子)類型, 甚至是內(nèi)置的對(duì)象(子)類型, 比如 Array 或 RegExp。沒有 class ..extends 語法時(shí), 想實(shí)現(xiàn)這一點(diǎn)是非常困難的, 基本上只有框架的作者才能搞清楚這一點(diǎn)。但是現(xiàn)在可以輕而易舉地做到!
平心而論,class 語法確實(shí)解決了典型原型風(fēng)格代碼中許多顯而易見的(語法)問題和缺點(diǎn)。
A.2 class 陷阱
然而,class 語法并沒有解決所有的問題, 在 JavaScript 中使用“類”設(shè)計(jì)模式仍然存在許多深層問題。
首先, 你可能會(huì)認(rèn)為 ES6 的 class 語法是向 JavaScript 中引入了一種新的“類”機(jī)制, 其實(shí)不是這樣。class 基本上只是現(xiàn)有[[Prototype]](委托?。C(jī)制的一種語法糖。
也就是說,class 并不會(huì)像傳統(tǒng)面向類的語言一樣在聲明時(shí)靜態(tài)復(fù)制所有行為。如果你(有意或無意)修改或者替換了父“類”中的一個(gè)方法, 那子“類”和所有實(shí)例都會(huì)受到影響,因?yàn)樗鼈冊(cè)诙x時(shí)并沒有進(jìn)行復(fù)制,只是使用基于[[Prototype]]的實(shí)時(shí)委托:
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" ——噢!
如果你已經(jīng)明白委托的原理所以并不會(huì)期望得到“類”的副本的話, 那這種行為才看起來比較合理。所以你需要問自己:為什么要使用本質(zhì)上不是類的 class 語法呢?ES6 中的 class 語法不是會(huì)讓傳統(tǒng)類和委托對(duì)象之間的區(qū)別更加難以發(fā)現(xiàn)和理解嗎?class 語法無法定義類成員屬性(只能定義方法),如果為了跟蹤實(shí)例之間共享狀態(tài)必須要這么做,那你只能使用丑陋的.prototype 語法,像這樣:
class C {
constructor() {
// 確保修改的是共享狀態(tài)而不是在實(shí)例上創(chuàng)建一個(gè)屏蔽屬性!
C.prototype.count++;
// this.count 可以通過委托實(shí)現(xiàn)我們想要的功能
console.log("Hello: " + this.count);
}
}
// 直接向prototype對(duì)象上添加一個(gè)共享狀態(tài)
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
這種方法最大的問題是, 它違背了 class 語法的本意, 在實(shí)現(xiàn)中暴露(泄露?。┝?prototype。
如果使用 this.count++的話, 我們會(huì)很驚訝地發(fā)現(xiàn)在對(duì)象 c1 和 c2 上都創(chuàng)建了.count 屬性, 而不是更新共享狀態(tài)。class 沒有辦法解決這個(gè)問題, 并且干脆就不提供相應(yīng)的語法支持,所以你根本就不應(yīng)該這樣做。
此外,class 語法仍然面臨意外屏蔽的問題:
class C {
constructor(id) {
// 噢,郁悶,我們的id屬性屏蔽了id()方法
this.id = id;
}
id() {
console.log("Id: " + id);
}
}
var c1 = new C("c1");
c1.id(); // TypeError -- c1.id現(xiàn)在是字符串"c1"
除此之外,super 也存在一些非常細(xì)微的問題。你可能認(rèn)為 super 的綁定方法和 this 類似(參見第 2 章),也就是說, 無論目前的方法在原型鏈中處于什么位置,super 總會(huì)綁定到鏈中的上一層。
然而, 出于性能考慮(this 綁定已經(jīng)是很大的開銷了),super 并不是動(dòng)態(tài)綁定的, 它會(huì)在聲明時(shí)“靜態(tài)”綁定。沒什么大不了的,是吧?
呃...... 可能, 可能不是這樣。如果你和大多數(shù) JavaScript 開發(fā)者一樣, 會(huì)用許多不同的方法把函數(shù)應(yīng)用在不同的(使用 class 定義的)對(duì)象上, 那你可能不知道, 每次執(zhí)行這些操作時(shí)都必須重新綁定 super。
此外, 根據(jù)應(yīng)用方式的不同,super 可能不會(huì)綁定到合適的對(duì)象(至少和你想的不一樣),所以你可能(寫作本書時(shí),TC39 正在討論這個(gè)話題)需要用 toMethod(..)來手動(dòng)綁定 super(類似用 bind(..)來綁定 this——參見第 2 章) 。
你已經(jīng)習(xí)慣了把方法應(yīng)用到不同的對(duì)象上,從而可以自動(dòng)利用 this 的隱式綁定規(guī)則(參見第 2 章) 。但是這對(duì)于 super 來說是行不通的。
思考下面代碼中 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"
如果你認(rèn)為 super 會(huì)動(dòng)態(tài)綁定(非常合理?。?,那你可能期望 super()會(huì)自動(dòng)識(shí)別出 E 委托了 D,所以 E.foo()中的 super()應(yīng)該調(diào)用 D.foo()。
但事實(shí)并不是這樣。出于性能考慮,super 并不像 this 一樣是晚綁定(late bound, 或者說動(dòng)態(tài)綁定)的, 它在[[HomeObject]].[[Prototype]]上,[[HomeObject]]會(huì)在創(chuàng)建時(shí)靜態(tài)綁定。
在本例中,super()會(huì)調(diào)用 P.foo(),因?yàn)榉椒ǖ腫[HomeObject]]仍然是 C,C.[[Prototype]]是 P。
確實(shí)可以手動(dòng)修改 super 綁定, 使用 toMethod(..)綁定或重新綁定方法的[[HomeObject]](就像設(shè)置對(duì)象的[[Prototype]]一樣!)就可以解決本例的問題:
var D = {
foo: function () { console.log("D.foo"); }
};
// 把 E 委托到 D
var E = Object.create(D);
// 手動(dòng)把foo的[[HomeObject]]綁定到E,E.[[Prototype]]是D, 所以 super()是D.foo()
E.foo = C.prototype.foo.toMethod(E, "foo");
E.foo(); // "D.foo"
toMethod(..)會(huì)復(fù)制方法并把 homeObject 當(dāng)作第一個(gè)參數(shù)(也就是我們傳入的 E),第二個(gè)參數(shù)(可選)是新方法的名稱(默認(rèn)是原方法名)。
除此之外, 開發(fā)者還有可能會(huì)遇到其他問題, 這有待觀察。無論如何, 對(duì)于引擎自動(dòng)綁定的 super 來說,你必須時(shí)刻警惕是否需要進(jìn)行手動(dòng)綁定。唉!
A.3 靜態(tài)大于動(dòng)態(tài)嗎
通過上面的這些特性可以看出,ES6 的 class 最大的問題在于,(像傳統(tǒng)的類一樣)它的語法有時(shí)會(huì)讓你認(rèn)為, 定義了一個(gè) class 后, 它就變成了一個(gè)(未來會(huì)被實(shí)例化的)東西的靜態(tài)定義。你會(huì)徹底忽略 C 是一個(gè)對(duì)象,是一個(gè)具體的可以直接交互的東西。
在傳統(tǒng)面向類的語言中, 類定義之后就不會(huì)進(jìn)行修改, 所以類的設(shè)計(jì)模式就不支持修改。但是 JavaScript 最強(qiáng)大的特性之一就是它的動(dòng)態(tài)性, 任何對(duì)象的定義都可以修改(除非你把它設(shè)置成不可變)。
class 似乎不贊成這樣做, 所以強(qiáng)制讓你使用丑陋的.prototype 語法以及 super 問題, 等等。而且對(duì)于這種動(dòng)態(tài)產(chǎn)生的問題,class 基本上都沒有提供解決方案。
換句話說,class 似乎想告訴你:“動(dòng)態(tài)太難實(shí)現(xiàn)了, 所以這可能不是個(gè)好主意。這里有一種看起來像靜態(tài)的語法,所以編寫靜態(tài)代碼吧?!?/p>
對(duì)于 JavaScript 來說這是多么悲傷的評(píng)論?。簞?dòng)態(tài)太難實(shí)現(xiàn)了, 我們假裝成靜態(tài)吧。(但是實(shí)際上并不是?。?/p>
總地來說,ES6 的 class 想偽裝成一種很好的語法問題的解決方案, 但是實(shí)際上卻讓問題更難解決而且讓 JavaScript 更加難以理解。
如果你使用.bind(..)函數(shù)來硬綁定函數(shù)(參見第 2 章),那么這個(gè)函數(shù)不會(huì)像普通函數(shù)那樣被 ES6 的 extend 擴(kuò)展到子類中。
A.4 小結(jié)
class 很好地偽裝成 JavaScript 中類和繼承設(shè)計(jì)模式的解決方案, 但是它實(shí)際上起到了反作用:它隱藏了許多問題并且?guī)砹烁喔?xì)小但是危險(xiǎn)的問題。
class 加深了過去 20 年中對(duì)于 JavaScript 中“類”的誤解, 在某些方面, 它產(chǎn)生的問題比解決的多,而且讓本來優(yōu)雅簡潔的[[Prototype]]機(jī)制變得非常別扭。
結(jié)論:如果 ES6 的 class 讓[[Prototype]]變得更加難用而且隱藏了 JavaScript 對(duì)象最重要的機(jī)制—— 對(duì)象之間的實(shí)時(shí)委托關(guān)聯(lián), 我們難道不應(yīng)該認(rèn)為 class 產(chǎn)生的問題比解決的多嗎?難道不應(yīng)該抵制這種設(shè)計(jì)模式嗎?
我無法替你回答這些問題, 但是我希望本書能從前所未有的深度分析這些問題, 并且能夠?yàn)槟闾峁┗卮饐栴}所需的所有信息。
最后聽一首悅耳的歌放松放松,回憶學(xué)到的東西。
點(diǎn)擊下面
播放音樂
長按二維碼關(guān)注,一起努力。
助力尋人啟事
微信公眾號(hào)回復(fù)?加群?一起學(xué)習(xí)。
