附錄A: ES6 `class`

特別說明摸屠,為便于查閱芍殖,文章轉(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 還解決了什么瞧剖?

  1. 不再有(某種意義上的拭嫁,繼續(xù)往下看!)指向 .prototype 的引用來弄亂代碼抓于。
  2. Button 被聲明為直接“繼承自”(也就是 extendsWidget做粤,而不是需要用 Object.create(..) 來替換 .prototype 鏈接的對象,或者用 __proto__Object.setPrototypeOf(..) 來設(shè)置它捉撮。
  3. super(..) 現(xiàn)在給了我們非常有用的 相對多態(tài) 的能力怕品,所以在鏈條上某一個層級上的任何方法,可以引用鏈條上相對上一層的同名方法呕缭。第四章中有一個關(guān)于構(gòu)造器的奇怪現(xiàn)象:構(gòu)造器不屬于它們的類堵泽,而且因此與類沒有聯(lián)系。super(..) 含有一個對此問題的解決方法 —— super() 會在構(gòu)造器內(nèi)部想如你期望的那樣工作恢总。
  4. class 字面語法對指定屬性沒有什么啟發(fā)(僅對方法有)迎罗。這看起來限制了某些東西,但是絕大多數(shù)情況下期望一個屬性(狀態(tài))存在于鏈條末端的“實例”以外的地方片仿,這通常是一個錯誤和令人詫異(因為這個狀態(tài)被隱含地在所有“實例”中“分享”)的纹安。所以,也可以說 class 語法防止你出現(xiàn)錯誤。
  5. 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++ 將會隱含地在 c1c2 兩個對象上創(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)怎樣動作(對 DE):

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)給出了 你自己回答這個問題 所需的信息蜓堕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市博其,隨后出現(xiàn)的幾起案子套才,更是在濱河造成了極大的恐慌,老刑警劉巖慕淡,帶你破解...
    沈念sama閱讀 211,423評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件背伴,死亡現(xiàn)場離奇詭異,居然都是意外死亡峰髓,警方通過查閱死者的電腦和手機傻寂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來携兵,“玉大人疾掰,你說我怎么就攤上這事⌒旖簦” “怎么了静檬?”我有些...
    開封第一講書人閱讀 157,019評論 0 348
  • 文/不壞的土叔 我叫張陵炭懊,是天一觀的道長。 經(jīng)常有香客問我拂檩,道長侮腹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,443評論 1 283
  • 正文 為了忘掉前任广恢,我火速辦了婚禮凯旋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钉迷。我一直安慰自己至非,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,535評論 6 385
  • 文/花漫 我一把揭開白布糠聪。 她就那樣靜靜地躺著荒椭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪舰蟆。 梳的紋絲不亂的頭發(fā)上趣惠,一...
    開封第一講書人閱讀 49,798評論 1 290
  • 那天,我揣著相機與錄音身害,去河邊找鬼味悄。 笑死,一個胖子當(dāng)著我的面吹牛塌鸯,可吹牛的內(nèi)容都是我干的侍瑟。 我是一名探鬼主播,決...
    沈念sama閱讀 38,941評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼丙猬,長吁一口氣:“原來是場噩夢啊……” “哼涨颜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起茧球,我...
    開封第一講書人閱讀 37,704評論 0 266
  • 序言:老撾萬榮一對情侶失蹤庭瑰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抢埋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弹灭,經(jīng)...
    沈念sama閱讀 44,152評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,494評論 2 327
  • 正文 我和宋清朗相戀三年揪垄,在試婚紗的時候發(fā)現(xiàn)自己被綠了鲤屡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,629評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡福侈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卢未,到底是詐尸還是另有隱情肪凛,我是刑警寧澤堰汉,帶...
    沈念sama閱讀 34,295評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站伟墙,受9級特大地震影響翘鸭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜戳葵,卻給世界環(huán)境...
    茶點故事閱讀 39,901評論 3 313
  • 文/蒙蒙 一就乓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拱烁,春花似錦生蚁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至擅笔,卻和暖如春志衣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背猛们。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評論 1 266
  • 我被黑心中介騙來泰國打工念脯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弯淘。 一個月前我還...
    沈念sama閱讀 46,333評論 2 360
  • 正文 我出身青樓绿店,卻偏偏與公主長得像,于是被迫代替她去往敵國和親耳胎。 傳聞我的和親對象是個殘疾皇子惯吕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,499評論 2 348

推薦閱讀更多精彩內(nèi)容