你不懂JS:this與對象原型 附錄A:ES6 `class`

官方中文版原文鏈接

感謝社區(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還解決了什么宪萄?

  1. 不再有(某種意義上的,繼續(xù)往下看Uザ琛)指向.prototype的引用來弄亂代碼拜英。
  2. Button被聲明為直接“繼承自”(也就是extendsWidget,而不是需要用Object.create(..)來替換.prototype鏈接的對象琅催,或者用__proto__Object.setPrototypeOf(..)來設置它居凶。
  3. super(..)現(xiàn)在給了我們非常有用的 相對多態(tài) 的能力,所以在鏈條上某一個層級上的任何方法藤抡,可以引用鏈條上相對上一層的同名方法侠碧。第四章中有一個關于構造器的奇怪現(xiàn)象:構造器不屬于它們的類,而且因此與類沒有聯(lián)系缠黍。super(..)含有一個對此問題的解決方法 —— super()會在構造器內部想正如你期望的那樣工作弄兜。
  4. class字面語法對指定屬性沒有什么啟發(fā)(僅對方法有)。這看起來限制了某些東西,但是絕大多數情況下期望一個屬性(狀態(tài))存在于鏈條末端的“實例”以外的地方替饿,這通常是一個錯誤和令人詫異(因為這個狀態(tài)被隱含地在所有“實例”中“分享”)的语泽。所以,也可以說class語法防止你出現(xiàn)錯誤盛垦。
  5. 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++將會隱含地在c1c2兩個對象上創(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應當怎樣動作(對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將會在調用時自動綁定凡蜻,你可能會期望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產生的麻煩比它解決的更多,并且將它貶低為一種反模式嗎哨鸭?

我真的不能幫你回答這個問題民宿。但我希望這本書已經在你從未經歷過的深度上完全地探索了這個問題,而且已經給出了 你自己回答這個問題 所需的信息像鸡。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末勘高,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子坟桅,更是在濱河造成了極大的恐慌华望,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仅乓,死亡現(xiàn)場離奇詭異赖舟,居然都是意外死亡,警方通過查閱死者的電腦和手機夸楣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門宾抓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來子漩,“玉大人,你說我怎么就攤上這事石洗〈逼茫” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵讲衫,是天一觀的道長缕棵。 經常有香客問我,道長涉兽,這世上最難降的妖魔是什么招驴? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮枷畏,結果婚禮上别厘,老公的妹妹穿的比我還像新娘。我一直安慰自己拥诡,他們只是感情好触趴,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渴肉,像睡著了一般雕蔽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宾娜,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天批狐,我揣著相機與錄音,去河邊找鬼前塔。 笑死嚣艇,一個胖子當著我的面吹牛,可吹牛的內容都是我干的华弓。 我是一名探鬼主播食零,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼寂屏!你這毒婦竟也來了贰谣?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤迁霎,失蹤者是張志新(化名)和其女友劉穎吱抚,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體考廉,經...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡秘豹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了昌粤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片既绕。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡啄刹,死狀恐怖,靈堂內的尸體忽然破棺而出凄贩,到底是詐尸還是另有隱情誓军,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布疲扎,位于F島的核電站昵时,受9級特大地震影響,放射性物質發(fā)生泄漏评肆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一非区、第九天 我趴在偏房一處隱蔽的房頂上張望瓜挽。 院中可真熱鬧,春花似錦征绸、人聲如沸久橙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淆衷。三九已至,卻和暖如春渤弛,著一層夾襖步出監(jiān)牢的瞬間祝拯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工她肯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留佳头,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓晴氨,卻偏偏與公主長得像康嘉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子籽前,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內容