原文 https://javascript.info/class...javascript
class 能夠 extends 自另外一個 class。這是一個不錯的語法,技術上基於原型繼承。java
要繼承一個對象,須要在 {..}
前指定 extends
和父對象。git
這個 Rabbit
繼承自 Animal
:github
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} stopped.`); } } // Inherit from Animal 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!
如你所見,如你所想,extend
關鍵字其實是在 Rabbit.prototype
添加 [Prototype]]
,引用到 Animal.prototype
。編程
因此如今 rabbit
既能夠訪問它本身的方法,也能夠訪問 Animal
的方法。數組
extends
後可跟表達式Class 語法的 `extends' 後接的不限於指定一個類,更能夠是表達式。安全
例如一個生成父類的函數:ide
function f(phrase) { return class { sayHi() { alert(phrase) } } } class User extends f("Hello") {} new User().sayHi(); // Hello
例子中,class User
繼承了 f('Hello')返回的結果。函數
對於高級編程模式,當咱們使用的類是根據許多條件使用函數來生成時,這就頗有用。ui
如今讓咱們進入下一步,重寫一個方法。到目前爲止,Rabbit
從 Animal
繼承了 stop
方法,this.speed = 0
。
若是咱們在 Rabbit
中指定了本身的 stop
,那麼會被優先使用:
class Rabbit extends Animal { stop() { // ...this will be used for rabbit.stop() } }
......但一般咱們不想徹底替代父方法,而是在父方法的基礎上調整或擴展其功能。咱們進行一些操做,讓它以前/以後或在過程當中調用父方法。
Class 爲此提供 super
關鍵字。
super.method(...)
調用父方法。super(...)
調用父構造函數(僅在 constructor 函數中)。例如,讓兔子在 stop
時自動隱藏:
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} stopped.`); } } class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } stop() { super.stop(); // call parent stop this.hide(); // and then hide } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.stop(); // White Rabbit stopped. White rabbit hides!
如今,Rabbit
的 stop
方法經過 super.stop()
調用父類的方法。
super
正如在 arrow-functions 一章中提到,箭頭函數沒有 super
。
它會從外部函數中獲取 super
。例如:
class Rabbit extends Animal { stop() { setTimeout(() => super.stop(), 1000); // call parent stop after 1sec } }
箭頭函數中的 super
與 stop()
中的相同,因此它按預期工做。若是咱們在這裏用普通函數,便會報錯:
// Unexpected super setTimeout(function() { super.stop() }, 1000);
對於構造函數來講,這有點棘手 tricky。
直到如今,Rabbit
都沒有本身的 constructor
。
Till now, Rabbit
did not have its own constructor
.
根據規範,若是一個類擴展了另外一個類而且沒有 constructor
,那麼會自動生成以下 constructor
:
class Rabbit extends Animal { // generated for extending classes without own constructors constructor(...args) { super(...args); } }
咱們能夠看到,它調用了父 constructor
傳遞全部參數。若是咱們不本身寫構造函數,就會發生這種狀況。
如今咱們將一個自定義構造函數添加到 Rabbit
中。除了name
,咱們還會設置 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; } // ... } // Doesn't work! let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
哎呦出錯了!如今咱們不能生成兔子了,爲何呢?
簡單來講:繼承類中的構造函數必須調用 super(...)
,(!)而且在使用 this
以前執行它。
...但爲何?這是什麼狀況?嗯...這個要求看起來確實奇怪。
如今咱們探討細節,讓你真正理解其中原因 ——
在JavaScript中,繼承了其餘類的構造函數比較特殊。在繼承類中,相應的構造函數被標記爲特殊的內部屬性 [[ConstructorKind]]:「derived」
。
區別在於:
因此若是咱們正在構造咱們本身的構造函數,那麼咱們必須調用 super
,不然具備 this
的對象將不被建立,並報錯。
對於 Rabbit
來講,咱們須要在使用 this
以前調用 super()
,以下所示:
class Animal { constructor(name) { this.speed = 0; this.name = name; } // ... } class Rabbit extends Animal { constructor(name, earLength) { super(name); this.earLength = earLength; } // ... } // now fine let rabbit = new Rabbit("White Rabbit", 10); alert(rabbit.name); // White Rabbit alert(rabbit.earLength); // 10
讓咱們再深刻理解 super
的底層實現,咱們會看到一些有趣的事情。
首先要說的是,以咱們迄今爲止學到的知識來看,實現 super 是不可能的。
那麼思考一下,這是什麼原理?當一個對象方法運行時,它將當前對象做爲 this
。若是咱們調用 super.method()
,那麼如何檢索 method
?很容易想到,咱們須要從當前對象的原型中取出 method
。從技術上講,咱們(或JavaScript引擎)能夠作到這一點嗎?
也許咱們能夠從 this
的 [[Prototype]] 中得到方法,就像 this .__ proto __.method
同樣?不幸的是,這是行不通的。
讓咱們試一試,簡單起見,咱們不使用 class 了,直接使用普通對象。
在這裏,rabbit.eat()
調用父對象的 animal.eat()
方法:
let animal = { name: "Animal", eat() { alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, name: "Rabbit", eat() { // that's how super.eat() could presumably work this.__proto__.eat.call(this); // (*) } }; rabbit.eat(); // Rabbit eats.
在 (*)
這一行,咱們從原型(animal
)中取出 eat
,並以當前對象的上下文中調用它。請注意,.call(this)
在這裏很重要,由於只寫 this .__ proto __.eat()
的話 eat
的調用對象將會是 animal
,而不是當前對象。
以上代碼的 alert
是正確的。
可是如今讓咱們再添加一個對象到原型鏈中,就要出事了:
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
噢,完蛋!調用 longEar.eat()
報錯了!
這緣由一眼可能看不透,但若是咱們跟蹤 longEar.eat()
調用,大概就知道爲何了。在 (*)
和 (**)
兩行中, this
的值是當前對象(longEar
)。重點來了:全部方法都將當前對象做爲 this
,而不是原型或其餘東西。
所以,在兩行 (*)
和 (**)
中,this.__ proto__
的值都是 rabbit
。他們都調用了 rabbit.eat
,因而就這麼無限循環下去。
狀況如圖:
1.在 longEar.eat()
裏面,(**)
行中調用了 rabbit.eat
,而且this = longEar
。
// inside longEar.eat() we have this = longEar this.__proto__.eat.call(this) // (**) // becomes longEar.__proto__.eat.call(this) // that is rabbit.eat.call(this);
2.而後在rabbit.eat
的 (*)
行中,咱們但願傳到原型鏈的下一層,可是 this = longEar
,因此 this .__ proto __.eat
又是 rabbit.eat
!
// inside rabbit.eat() we also have this = longEar this.__proto__.eat.call(this) // (*) // becomes longEar.__proto__.eat.call(this) // or (again) rabbit.eat.call(this);
rabbit.eat
在無盡循環調動,沒法進入下一層。這個問題不能簡單使用 this
解決。
[[HomeObject]]
爲了提供解決方案,JavaScript 爲函數添加了一個特殊的內部屬性:[[HomeObject]]
。
當函數被指定爲類或對象方法時,其 [[HomeObject]]
屬性爲該對象。
這實際上違反了 unbind 函數的思想,由於方法記住了它們的對象。而且 [[HomeObject]]
不能被改變,因此這是永久 bind(綁定)。因此在 JavaScript 這是一個很大的變化。
可是這種改變是安全的。 [[HomeObject]]
僅用於在 super
中獲取下一層原型。因此它不會破壞兼容性。
讓咱們來看看它是如何在 super
中運做的:
let animal = { name: "Animal", eat() { // [[HomeObject]] == animal alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, name: "Rabbit", eat() { // [[HomeObject]] == rabbit super.eat(); } }; let longEar = { __proto__: rabbit, name: "Long Ear", eat() { // [[HomeObject]] == longEar super.eat(); } }; longEar.eat(); // Long Ear eats.
每一個方法都會在內部 [[HomeObject]]
屬性中記住它的對象。而後 super
使用它來解析原型。
在類和普通對象中定義的方法中都定義了 [[HomeObject]]
,可是對於對象,必須使用:method()
而不是 "method: function()"
。
在下面的例子中,使用非方法語法(non-method syntax)進行比較。這麼作沒有設置 [[HomeObject]]
屬性,繼承也不起做用:
let animal = { eat: function() { // should be the short syntax: eat() {...} // ... } }; let rabbit = { __proto__: animal, eat: function() { super.eat(); } }; rabbit.eat(); // Error calling super (because there's no [[HomeObject]])
class
語法也支持靜態屬性的繼承。
例如:
class Animal { constructor(name, speed) { this.speed = speed; this.name = name; } run(speed = 0) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } static compare(animalA, animalB) { return animalA.speed - animalB.speed; } } // Inherit from Animal class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbits = [ new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5) ]; rabbits.sort(Rabbit.compare); rabbits[0].run(); // Black Rabbit runs with speed 5.
如今咱們能夠調用 Rabbit.compare
,假設繼承的 Animal.compare
將被調用。
它是如何工做的?再次使用原型。正如你猜到的那樣,extends 一樣給 Rabbit
提供了引用到 Animal
的 [Prototype]
。
因此,Rabbit
函數如今繼承 Animal
函數。Animal
自帶引用到 Function.prototype
的 [[Prototype]]
(由於它不 extend
其餘類)。
看看這裏:
class Animal {} class Rabbit extends Animal {} // for static propertites and methods alert(Rabbit.__proto__ === Animal); // true // and the next step is Function.prototype alert(Animal.__proto__ === Function.prototype); // true // that's in addition to the "normal" prototype chain for object methods alert(Rabbit.prototype.__proto__ === Animal.prototype);
這樣 Rabbit
能夠訪問 Animal
的全部靜態方法。
請注意,內置類沒有靜態 [[Prototype]]
引用。例如,Object
具備 Object.defineProperty
,Object.keys
等方法,但 Array
,Date
不會繼承它們。
Date
和 Object
的結構:
Date
和 Object
之間毫無關聯,他們獨立存在,不過 Date.prototype
繼承於 Object.prototype
,僅此而已。
形成這個狀況是由於 JavaScript 在設計初期沒有考慮使用 class 語法和繼承靜態方法。
Array,Map 等內置類也能夠擴展。
舉個例子,PowerArray
繼承自原生 Array
:
// add one more method to it (can do more) class PowerArray extends Array { isEmpty() { return this.length === 0; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false let filteredArr = arr.filter(item => item >= 10); alert(filteredArr); // 10, 50 alert(filteredArr.isEmpty()); // false
請注意一件很是有趣的事情。像 filter
,map
和其餘內置方法 - 返回新的繼承類型的對象。他們依靠 constructor
屬性來作到這一點。
在上面的例子中,
arr.constructor === PowerArray
因此當調用 arr.filter()
時,它自動建立新的結果數組,就像 new PowerArray
同樣,因而咱們能夠繼續使用 PowerArray 的方法。
咱們甚至能夠自定義這種行爲。若是存在靜態 getter Symbol.species
,返回新建對象使用的 constructor。
下面的例子中,因爲 Symbol.species
的存在,map
,filter
等內置方法將返回普通的數組:
class PowerArray extends Array { isEmpty() { return this.length === 0; } // built-in methods will use this as the constructor static get [Symbol.species]() { return Array; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false // filter creates new array using arr.constructor[Symbol.species] as constructor let filteredArr = arr.filter(item => item >= 10); // filteredArr is not PowerArray, but Array alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
咱們能夠在其餘 key 使用 Symbol.species
,能夠用於剝離結果值中的無用方法,或是增長其餘方法。