ES6 Class 繼承與 super

原文 https://javascript.info/class...javascript

Class 繼承與 super

class 能夠 extends 自另外一個 class。這是一個不錯的語法,技術上基於原型繼承。java

要繼承一個對象,須要在 {..} 前指定 extends 和父對象。git

這個 Rabbit 繼承自 Animalgithub

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

重寫一個方法

如今讓咱們進入下一步,重寫一個方法。到目前爲止,RabbitAnimal 繼承了 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!

如今,Rabbitstop 方法經過 super.stop() 調用父類的方法。

箭頭函數無 super

正如在 arrow-functions 一章中提到,箭頭函數沒有 super

它會從外部函數中獲取 super。例如:

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // call parent stop after 1sec
  }
}

箭頭函數中的 superstop() 中的相同,因此它按預期工做。若是咱們在這裏用普通函數,便會報錯:

// 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」

區別在於:

  • 當一個普通的構造函數運行時,它會建立一個空對象做爲 this,而後繼續運行。
  • 可是當派生的構造函數運行時,與上面說的不一樣,它期望父構造函數來完成這項工做。

因此若是咱們正在構造咱們本身的構造函數,那麼咱們必須調用 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 的實現與 [[HomeObject]]

讓咱們再深刻理解 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);
  1. ...所以 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.definePropertyObject.keys等方法,但 ArrayDate 不會繼承它們。

DateObject 的結構:

DateObject 之間毫無關聯,他們獨立存在,不過 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

請注意一件很是有趣的事情。像 filtermap 和其餘內置方法 - 返回新的繼承類型的對象。他們依靠 constructor 屬性來作到這一點。

在上面的例子中,

arr.constructor === PowerArray

因此當調用 arr.filter() 時,它自動建立新的結果數組,就像 new PowerArray 同樣,因而咱們能夠繼續使用 PowerArray 的方法。

咱們甚至能夠自定義這種行爲。若是存在靜態 getter Symbol.species,返回新建對象使用的 constructor。

下面的例子中,因爲 Symbol.species 的存在,mapfilter等內置方法將返回普通的數組:

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,能夠用於剝離結果值中的無用方法,或是增長其餘方法。

相關文章
相關標籤/搜索