類繼承是一個(gè)類擴(kuò)展另一個(gè)類的一種方式。
因此,我們可以在現(xiàn)有功能之上創(chuàng)建新功能。
假設(shè)我們有 class Animal
:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
let animal = new Animal("My animal");
這是我們對(duì)對(duì)象 animal
和 class Animal
的圖形化表示:
……然后我們想創(chuàng)建另一個(gè) class Rabbit
:
因?yàn)?rabbit 是 animal,所以 class Rabbit
應(yīng)該是基于 class Animal
的,可以訪問 animal 的方法,以便 rabbit 可以做“一般”動(dòng)物可以做的事兒。
擴(kuò)展另一個(gè)類的語(yǔ)法是:class Child extends Parent
。
讓我們創(chuàng)建一個(gè)繼承自 Animal
的 class Rabbit
:
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!
class Rabbit
的對(duì)象可以訪問例如 rabbit.hide()
等 Rabbit
的方法,還可以訪問例如 rabbit.run()
等 Animal
的方法。
在內(nèi)部,關(guān)鍵字 extends
使用了很好的舊的原型機(jī)制進(jìn)行工作。它將 Rabbit.prototype.[[Prototype]]
設(shè)置為 Animal.prototype
。所以,如果在 Rabbit.prototype
中找不到一個(gè)方法,JavaScript 就會(huì)從 Animal.prototype
中獲取該方法。
例如,要查找 rabbit.run
方法,JavaScript 引擎會(huì)進(jìn)行如下檢查(如圖所示從下到上):
rabbit
?(沒有 ?run
?)。Rabbit.prototype
?(有 ?hide
?,但沒有 ?run
?)。extends
?)?Animal.prototype
?,在這兒找到了 ?run
? 方法。我們可以回憶一下 原生的原型 這一章的內(nèi)容,JavaScript 內(nèi)建對(duì)象同樣也使用原型繼承。例如,Date.prototype.[[Prototype]]
是 Object.prototype
。這就是為什么日期可以訪問通用對(duì)象的方法。
在 ?
extends
? 后允許任意表達(dá)式類語(yǔ)法不僅允許指定一個(gè)類,在
extends
后可以指定任意表達(dá)式。
例如,一個(gè)生成父類的函數(shù)調(diào)用:
function f(phrase) { return class { sayHi() { alert(phrase); } }; } class User extends f("Hello") {} new User().sayHi(); // Hello
這里
class User
繼承自f("Hello")
的結(jié)果。
這對(duì)于高級(jí)編程模式,例如當(dāng)我們根據(jù)許多條件使用函數(shù)生成類,并繼承它們時(shí)來說可能很有用。
現(xiàn)在,讓我們繼續(xù)前行并嘗試重寫一個(gè)方法。默認(rèn)情況下,所有未在 class Rabbit
中指定的方法均從 class Animal
中直接獲取。
但是如果我們?cè)?nbsp;Rabbit
中指定了我們自己的方法,例如 stop()
,那么將會(huì)使用它:
class Rabbit extends Animal {
stop() {
// ……現(xiàn)在這個(gè)將會(huì)被用作 rabbit.stop()
// 而不是來自于 class Animal 的 stop()
}
}
然而通常,我們不希望完全替換父類的方法,而是希望在父類方法的基礎(chǔ)上進(jìn)行調(diào)整或擴(kuò)展其功能。我們?cè)谖覀兊姆椒ㄖ凶鲆恍┦聝海窃谒盎蛑蠡蛟谶^程中會(huì)調(diào)用父類方法。
Class 為此提供了 "super"
關(guān)鍵字。
super.method(...)
? 來調(diào)用一個(gè)父類方法。super(...)
? 來調(diào)用一個(gè)父類 constructor(只能在我們的 constructor 中)。例如,讓我們的 rabbit 在停下來的時(shí)候自動(dòng) hide:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} stands still.`);
}
}
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
stop() {
super.stop(); // 調(diào)用父類的 stop
this.hide(); // 然后 hide
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stands still. White Rabbit hides!
現(xiàn)在,Rabbit
在執(zhí)行過程中調(diào)用父類的 super.stop()
方法,所以 Rabbit
也具有了 stop
方法。
箭頭函數(shù)沒有 ?
super
?正如我們?cè)?nbsp;深入理解箭頭函數(shù) 一章中所提到的,箭頭函數(shù)沒有
super
。
如果被訪問,它會(huì)從外部函數(shù)獲取。例如:
class Rabbit extends Animal { stop() { setTimeout(() => super.stop(), 1000); // 1 秒后調(diào)用父類的 stop } }
箭頭函數(shù)中的
super
與stop()
中的是一樣的,所以它能按預(yù)期工作。如果我們?cè)谶@里指定一個(gè)“普通”函數(shù),那么將會(huì)拋出錯(cuò)誤:
// 意料之外的 super setTimeout(function() { super.stop() }, 1000);
對(duì)于重寫 constructor 來說,則有點(diǎn)棘手。
到目前為止,Rabbit
還沒有自己的 constructor
。
根據(jù) 規(guī)范,如果一個(gè)類擴(kuò)展了另一個(gè)類并且沒有 constructor
,那么將生成下面這樣的“空” constructor
:
class Rabbit extends Animal {
// 為沒有自己的 constructor 的擴(kuò)展類生成的
constructor(...args) {
super(...args);
}
}
正如我們所看到的,它調(diào)用了父類的 constructor
,并傳遞了所有的參數(shù)。如果我們沒有寫自己的 constructor,就會(huì)出現(xiàn)這種情況。
現(xiàn)在,我們給 Rabbit
添加一個(gè)自定義的 constructor。除了 name
之外,它還會(huì)指定 earLength
。
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
this.speed = 0;
this.name = name;
this.earLength = earLength;
}
// ...
}
// 不工作!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
哎呦!我們得到了一個(gè)報(bào)錯(cuò)?,F(xiàn)在我們沒法新建 rabbit。是什么地方出錯(cuò)了?
簡(jiǎn)短的解釋是:
繼承類的 constructor 必須調(diào)用 super(...)
,并且 (!) 一定要在使用 this
之前調(diào)用。
……但這是為什么呢?這里發(fā)生了什么?確實(shí),這個(gè)要求看起來很奇怪。
當(dāng)然,本文會(huì)給出一個(gè)解釋。讓我們深入細(xì)節(jié),這樣你就可以真正地理解發(fā)生了什么。
在 JavaScript 中,繼承類(所謂的“派生構(gòu)造器”,英文為 “derived constructor”)的構(gòu)造函數(shù)與其他函數(shù)之間是有區(qū)別的。派生構(gòu)造器具有特殊的內(nèi)部屬性 [[ConstructorKind]]:"derived"
。這是一個(gè)特殊的內(nèi)部標(biāo)簽。
該標(biāo)簽會(huì)影響它的 new
行為:
new
? 執(zhí)行一個(gè)常規(guī)函數(shù)時(shí),它將創(chuàng)建一個(gè)空對(duì)象,并將這個(gè)空對(duì)象賦值給 ?this
?。因此,派生的 constructor 必須調(diào)用 super
才能執(zhí)行其父類(base)的 constructor,否則 this
指向的那個(gè)對(duì)象將不會(huì)被創(chuàng)建。并且我們會(huì)收到一個(gè)報(bào)錯(cuò)。
為了讓 Rabbit
的 constructor 可以工作,它需要在使用 this
之前調(diào)用 super()
,就像下面這樣:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
this.earLength = earLength;
}
// ...
}
// 現(xiàn)在可以了
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10
高階要點(diǎn)
這個(gè)要點(diǎn)假設(shè)你對(duì)類已經(jīng)有了一定的經(jīng)驗(yàn),或許是在其他編程語(yǔ)言中。
這里提供了一個(gè)更好的視角來窺探這門語(yǔ)言,且解釋了它的行為為什么可能會(huì)是 bugs 的來源(但不是非常頻繁)。
如果你發(fā)現(xiàn)這難以理解,什么都別管,繼續(xù)往下閱讀,之后有機(jī)會(huì)再回來看。
我們不僅可以重寫方法,還可以重寫類字段。
不過,當(dāng)我們?cè)诟割悩?gòu)造器中訪問一個(gè)被重寫的字段時(shí),有一個(gè)詭異的行為,這與絕大多數(shù)其他編程語(yǔ)言都很不一樣。
請(qǐng)思考此示例:
class Animal {
name = 'animal';
constructor() {
alert(this.name); // (*)
}
}
class Rabbit extends Animal {
name = 'rabbit';
}
new Animal(); // animal
new Rabbit(); // animal
這里,Rabbit
繼承自 Animal
,并且用它自己的值重寫了 name
字段。
因?yàn)?nbsp;Rabbit
中沒有自己的構(gòu)造器,所以 Animal
的構(gòu)造器被調(diào)用了。
有趣的是在這兩種情況下:new Animal()
和 new Rabbit()
,在 (*)
行的 alert
都打印了 animal
。
換句話說,父類構(gòu)造器總是會(huì)使用它自己字段的值,而不是被重寫的那一個(gè)。
古怪的是什么呢?
如果這還不清楚,那么讓我們用方法來進(jìn)行比較。
這里是相同的代碼,但是我們調(diào)用 this.showName()
方法而不是 this.name
字段:
class Animal {
showName() { // 而不是 this.name = 'animal'
alert('animal');
}
constructor() {
this.showName(); // 而不是 alert(this.name);
}
}
class Rabbit extends Animal {
showName() {
alert('rabbit');
}
}
new Animal(); // animal
new Rabbit(); // rabbit
請(qǐng)注意:這時(shí)的輸出是不同的。
這才是我們本來所期待的結(jié)果。當(dāng)父類構(gòu)造器在派生的類中被調(diào)用時(shí),它會(huì)使用被重寫的方法。
……但對(duì)于類字段并非如此。正如前文所述,父類構(gòu)造器總是使用父類的字段。
這里為什么會(huì)有這樣的區(qū)別呢?
實(shí)際上,原因在于字段初始化的順序。類字段是這樣初始化的:
super()
? 后立刻初始化。在我們的例子中,Rabbit
是派生類,里面沒有 constructor()
。正如先前所說,這相當(dāng)于一個(gè)里面只有 super(...args)
的空構(gòu)造器。
所以,new Rabbit()
調(diào)用了 super()
,因此它執(zhí)行了父類構(gòu)造器,并且(根據(jù)派生類規(guī)則)只有在此之后,它的類字段才被初始化。在父類構(gòu)造器被執(zhí)行的時(shí)候,Rabbit
還沒有自己的類字段,這就是為什么 Animal
類字段被使用了。
這種字段與方法之間微妙的區(qū)別只特定于 JavaScript。
幸運(yùn)的是,這種行為僅在一個(gè)被重寫的字段被父類構(gòu)造器使用時(shí)才會(huì)顯現(xiàn)出來。接下來它會(huì)發(fā)生的東西可能就比較難理解了,所以我們要在這里對(duì)此行為進(jìn)行解釋。
如果出問題了,我們可以通過使用方法或者 getter/setter 替代類字段,來修復(fù)這個(gè)問題。
進(jìn)階內(nèi)容
如果你是第一次閱讀本教程,那么則可以跳過本節(jié)。
這是關(guān)于繼承和 ?
super
? 背后的內(nèi)部機(jī)制。
讓我們更深入地研究 super
。我們將在這個(gè)過程中發(fā)現(xiàn)一些有趣的事兒。
首先要說的是,從我們迄今為止學(xué)到的知識(shí)來看,super
是不可能運(yùn)行的。
的確是這樣,讓我們問問自己,以技術(shù)的角度它是如何工作的?當(dāng)一個(gè)對(duì)象方法執(zhí)行時(shí),它會(huì)將當(dāng)前對(duì)象作為 this
。隨后如果我們調(diào)用 super.method()
,那么引擎需要從當(dāng)前對(duì)象的原型中獲取 method
。但這是怎么做到的?
這個(gè)任務(wù)看起來是挺容易的,但其實(shí)并不簡(jiǎn)單。引擎知道當(dāng)前對(duì)象的 this
,所以它可以獲取父 method
作為 this.__proto__.method
。不幸的是,這個(gè)“天真”的解決方法是行不通的。
讓我們演示一下這個(gè)問題。簡(jiǎn)單起見,我們使用普通對(duì)象而不使用類。
如果你不想知道更多的細(xì)節(jié)知識(shí),你可以跳過此部分,并轉(zhuǎn)到下面的 [[HomeObject]]
小節(jié)。這沒關(guān)系的。但如果你感興趣,想學(xué)習(xí)更深入的知識(shí),那就繼續(xù)閱讀吧。
在下面的例子中,rabbit.__proto__ = animal
?,F(xiàn)在讓我們嘗試一下:在 rabbit.eat()
我們將會(huì)使用 this.__proto__
調(diào)用 animal.eat()
:
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() {
// 這就是 super.eat() 可以大概工作的方式
this.__proto__.eat.call(this); // (*)
}
};
rabbit.eat(); // Rabbit eats.
在 (*)
這一行,我們從原型(animal
)中獲取 eat
,并在當(dāng)前對(duì)象的上下文中調(diào)用它。請(qǐng)注意,.call(this)
在這里非常重要,因?yàn)楹?jiǎn)單的調(diào)用 this.__proto__.eat()
將在原型的上下文中執(zhí)行 eat
,而非當(dāng)前對(duì)象。
在上面的代碼中,它確實(shí)按照了期望運(yùn)行:我們獲得了正確的 alert
。
現(xiàn)在,讓我們?cè)谠玩溕显偬砑右粋€(gè)對(duì)象。我們將看到這件事是如何被打破的:
let animal = {
name: "Animal",
eat() {
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
eat() {
// ...bounce around rabbit-style and call parent (animal) method
this.__proto__.eat.call(this); // (*)
}
};
let longEar = {
__proto__: rabbit,
eat() {
// ...do something with long ears and call parent (rabbit) method
this.__proto__.eat.call(this); // (**)
}
};
longEar.eat(); // Error: Maximum call stack size exceeded
代碼無法再運(yùn)行了!我們可以看到,在試圖調(diào)用 longEar.eat()
時(shí)拋出了錯(cuò)誤。
原因可能不那么明顯,但是如果我們跟蹤 longEar.eat()
調(diào)用,就可以發(fā)現(xiàn)原因。在 (*)
和 (**)
這兩行中,this
的值都是當(dāng)前對(duì)象(longEar
)。這是至關(guān)重要的一點(diǎn):所有的對(duì)象方法都將當(dāng)前對(duì)象作為 this
,而非原型或其他什么東西。
因此,在 (*)
和 (**)
這兩行中,this.__proto__
的值是完全相同的:都是 rabbit
。它們倆都調(diào)用的是 rabbit.eat
,它們?cè)诓煌5匮h(huán)調(diào)用自己,而不是在原型鏈上向上尋找方法。
這張圖介紹了發(fā)生的情況:
longEar.eat()
中,(**)
這一行調(diào)用 rabbit.eat
并為其提供 this=longEar
。// 在 longEar.eat() 中我們有 this = longEar
this.__proto__.eat.call(this) // (**)
// 變成了
longEar.__proto__.eat.call(this)
// 也就是
rabbit.eat.call(this);
rabbit.eat
的 (*)
行中,我們希望將函數(shù)調(diào)用在原型鏈上向更高層傳遞,但是 this=longEar
,所以 this.__proto__.eat
又是 rabbit.eat
!// 在 rabbit.eat() 中我們依然有 this = longEar
this.__proto__.eat.call(this) // (*)
// 變成了
longEar.__proto__.eat.call(this)
// 或(再一次)
rabbit.eat.call(this);
rabbit.eat
? 在不停地循環(huán)調(diào)用自己,因此它無法進(jìn)一步地提升。這個(gè)問題沒法僅僅通過使用 this
來解決。
為了提供解決方法,JavaScript 為函數(shù)添加了一個(gè)特殊的內(nèi)部屬性:[[HomeObject]]
。
當(dāng)一個(gè)函數(shù)被定義為類或者對(duì)象方法時(shí),它的 [[HomeObject]]
屬性就成為了該對(duì)象。
然后 super
使用它來解析(resolve)父原型及其方法。
讓我們看看它是怎么工作的,首先,對(duì)于普通對(duì)象:
let animal = {
name: "Animal",
eat() { // animal.eat.[[HomeObject]] == animal
alert(`${this.name} eats.`);
}
};
let rabbit = {
__proto__: animal,
name: "Rabbit",
eat() { // rabbit.eat.[[HomeObject]] == rabbit
super.eat();
}
};
let longEar = {
__proto__: rabbit,
name: "Long Ear",
eat() { // longEar.eat.[[HomeObject]] == longEar
super.eat();
}
};
// 正確執(zhí)行
longEar.eat(); // Long Ear eats.
它基于 [[HomeObject]]
運(yùn)行機(jī)制按照預(yù)期執(zhí)行。一個(gè)方法,例如 longEar.eat
,知道其 [[HomeObject]]
并且從其原型中獲取父方法。并沒有使用 this
。
正如我們之前所知道的,函數(shù)通常都是“自由”的,并沒有綁定到 JavaScript 中的對(duì)象。正因如此,它們可以在對(duì)象之間復(fù)制,并用另外一個(gè) this
調(diào)用它。
[[HomeObject]]
的存在違反了這個(gè)原則,因?yàn)榉椒ㄓ涀×怂鼈兊膶?duì)象。[[HomeObject]]
不能被更改,所以這個(gè)綁定是永久的。
在 JavaScript 語(yǔ)言中 [[HomeObject]]
僅被用于 super
。所以,如果一個(gè)方法不使用 super
,那么我們?nèi)匀豢梢砸曀鼮樽杂傻牟⑶铱稍趯?duì)象之間復(fù)制。但是用了 super
再這樣做可能就會(huì)出錯(cuò)。
下面是復(fù)制后錯(cuò)誤的 super
結(jié)果的示例:
let animal = {
sayHi() {
alert(`I'm an animal`);
}
};
// rabbit 繼承自 animal
let rabbit = {
__proto__: animal,
sayHi() {
super.sayHi();
}
};
let plant = {
sayHi() {
alert("I'm a plant");
}
};
// tree 繼承自 plant
let tree = {
__proto__: plant,
sayHi: rabbit.sayHi // (*)
};
tree.sayHi(); // I'm an animal (?!?)
調(diào)用 tree.sayHi()
顯示 “I’m an animal”。這絕對(duì)是錯(cuò)誤的。
原因很簡(jiǎn)單:
(*)
? 行,?tree.sayHi
? 方法是從 ?rabbit
? 復(fù)制而來。也許我們只是想避免重復(fù)代碼?[[HomeObject]]
? 是 ?rabbit
?,因?yàn)樗窃?nbsp;?rabbit
? 中創(chuàng)建的。沒有辦法修改 ?[[HomeObject]]
?。tree.sayHi()
? 內(nèi)具有 ?super.sayHi()
?。它從 ?rabbit
? 中上溯,然后從 ?animal
? 中獲取方法。這是發(fā)生的情況示意圖:
[[HomeObject]]
是為類和普通對(duì)象中的方法定義的。但是對(duì)于對(duì)象而言,方法必須確切指定為 method()
,而不是 "method: function()"
。
這個(gè)差別對(duì)我們來說可能不重要,但是對(duì) JavaScript 來說卻非常重要。
在下面的例子中,使用非方法(non-method)語(yǔ)法進(jìn)行了比較。未設(shè)置 [[HomeObject]]
屬性,并且繼承無效:
let animal = {
eat: function() { // 這里是故意這樣寫的,而不是 eat() {...
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
super.eat();
}
};
rabbit.eat(); // 錯(cuò)誤調(diào)用 super(因?yàn)檫@里沒有 [[HomeObject]])
class Child extends Parent
?:Child.prototype.__proto__
? 將是 ?Parent.prototype
?,所以方法會(huì)被繼承。this
? 之前,我們必須在 ?Child
? 的 constructor 中將父 constructor 調(diào)用為 ?super()
?。Child
? 方法中使用 ?super.method()
? 來調(diào)用 ?Parent
? 方法。[[HomeObject]]
? 屬性中記住了它們的類/對(duì)象。這就是 ?super
? 如何解析父方法的。super
? 的方法從一個(gè)對(duì)象復(fù)制到另一個(gè)對(duì)象是不安全的。補(bǔ)充:
this
? 或 ?super
?,所以它們能融入到就近的上下文中,像透明似的。這里有一份 ?Rabbit
? 擴(kuò)展 ?Animal
? 的代碼。
不幸的是,?Rabbit
? 對(duì)象無法被創(chuàng)建。是哪里出錯(cuò)了呢?請(qǐng)解決它。
class Animal {
constructor(name) {
this.name = name;
}
}
class Rabbit extends Animal {
constructor(name) {
this.name = name;
this.created = Date.now();
}
}
let rabbit = new Rabbit("White Rabbit"); // Error: this is not defined
alert(rabbit.name);
這是因?yàn)樽宇惖?constructor 必須調(diào)用 super()
。
這里是修正后的代碼:
class Animal {
constructor(name) {
this.name = name;
}
}
class Rabbit extends Animal {
constructor(name) {
super(name);
this.created = Date.now();
}
}
let rabbit = new Rabbit("White Rabbit"); // 現(xiàn)在好了
alert(rabbit.name); // White Rabbit
我們獲得了一個(gè) ?Clock
? 類。到目前為止,它每秒都會(huì)打印一次時(shí)間。
class Clock {
constructor({ template }) {
this.template = template;
}
render() {
let date = new Date();
let hours = date.getHours();
if (hours < 10) hours = '0' + hours;
let mins = date.getMinutes();
if (mins < 10) mins = '0' + mins;
let secs = date.getSeconds();
if (secs < 10) secs = '0' + secs;
let output = this.template
.replace('h', hours)
.replace('m', mins)
.replace('s', secs);
console.log(output);
}
stop() {
clearInterval(this.timer);
}
start() {
this.render();
this.timer = setInterval(() => this.render(), 1000);
}
}
創(chuàng)建一個(gè)繼承自 Clock
的新的類 ExtendedClock
,并添加參數(shù) precision
— 每次 “ticks” 之間間隔的毫秒數(shù),默認(rèn)是 1000
(1 秒)。
extended-clock.js
? 文件里。clock.js
?。請(qǐng)擴(kuò)展它。class ExtendedClock extends Clock {
constructor(options) {
super(options);
let { precision = 1000 } = options;
this.precision = precision;
}
start() {
this.render();
this.timer = setInterval(() => this.render(), this.precision);
}
};
更多建議: