继承包括私有属性的继承和公共属性的继承

  1. 想要扩展一个类:class Child extends Parent
    • 这意味着 Child.prototype.__proto__ 将是 Parent.prototype,所以方法会被继承。
  2. 重写一个 constructor:
    • 在使用 this 之前,我们必须在 Child 的 constructor 中将父 constructor 调用为 super()
  3. 重写一个方法:
    • 我们可以在一个 Child 方法中使用 super.method() 来调用 Parent 方法。
  4. 内部:
    • 方法在内部的 [[HomeObject]] 属性中记住了它们的类/对象。这就是 super 如何解析父方法的。
    • 因此,将一个带有 super 的方法从一个对象复制到另一个对象是不安全的。
    • 对于对象而言,方法必须确切指定为 method(),而不是 "method: function()"

      extends关键字

      关键字 extends 使用了很好的旧的原型机制进行工作。它将 Rabbit.prototype.[[Prototype]] 设置为 Animal.prototype
      所以,如果在 Rabbit.prototype 中找不到一个方法,JavaScript 就会从 Animal.prototype 中获取该方法。
      image.png

      重写方法

      Rabbit 中指定了自己的方法,例如 stop(),那么将会使用它:
      但通常来说,我们不希望完全替换父类的方法,而是希望在父类方法的基础上进行调整或扩展其功能。

Class 为此提供了 "super" 关键字。

  • 执行 super.method(...) 来调用一个父类方法。
  • 执行 super(...) 来调用一个父类 constructor(只能在子类的constructor 中)。

    箭头函数没有 **super**

  1. // 父类Class
  2. class Animal {
  3. // ...其他属性和方法
  4. stop() {
  5. this.speed = 0;
  6. alert(`${this.name} stands still.`);
  7. }
  8. }
  9. // 子类Class
  10. class Rabbit extends Animal {
  11. hide() {
  12. alert(`${this.name} hides!`);
  13. }
  14. stop() {
  15. super.stop(); // 调用父类的 stop
  16. this.hide(); // 然后 hide
  17. }
  18. }

重写 constructor

根据 规范,如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的“空” constructor

  • 它调用了父类的 constructor ,在这个构造函数中,调用了 super 方法并传递了所有的参数
    1. class Rabbit extends Animal {
    2. // 为没有自己的 constructor 的扩展类生成的
    3. constructor(...args) {
    4. super(...args); //
    5. }
    6. }

    但当给 Rabbit 添加一个自定义的 constructor。除了 name 之外,它还会指定 earLength
    得到了一个报错, 没法新建 rabbit
    解释: **继承类的 constructor 必须调用 super(...),并且 (!) 一定要在使用 this 之前调用。** ```javascript class Animal { constructor(name) { this.name = name; } }

class Rabbit extends Animal { constructor(name, earLength) { this.name = name; this.earLength = earLength; } }

// 不工作! let rabbit = new Rabbit(“White Rabbit”, 10); // Error: this is not defined.

  1. <a name="3tCvR"></a>
  2. ### constructor 里为什么必须调用 `super(...)`?
  3. 在 JavaScript 中,继承类(所谓的“派生构造器”)的构造函数与其他函数之间是有区别的。
  4. - 派生构造器具有特殊的**内部属性** **`[[ConstructorKind]]:"derived"`**。这是一个特殊的内部标签。
  5. 该标签会影响它的 `new` 行为:
  6. - 当通过 `new` 执行一个**常规函数**时,它将**创建一个空对象**,并将这个空对象赋值给 `this`。
  7. - 但当**继承的 constructor **执行时,它不会执行此操作。它**期望父类的 constructor 来完成这项工作。**
  8. 因此,**派生的 constructor 必须调用 ****`super`**** 才能执行其父类(base)的 constructor,否则 ****`this`**** 指向的那个对象将不会被创建。并且会收到一个报错。**
  9. ```javascript
  10. class Rabbit extends Animal {
  11. constructor(name, earLength) {
  12. super(name);
  13. this.earLength = earLength;
  14. }
  15. }
  16. // 现在可以了
  17. let rabbit = new Rabbit("White Rabbit", 10);
  18. alert(rabbit.name); // White Rabbit

重写类字段

不仅可以重写方法,还可以重写类字段
当我们访问在父类构造器中的一个被重写的字段时,这里会有一个诡异的行为;

  • 这里,Rabbit 继承自 Animal,并且用它自己的值重写了 name 字段。 ```javascript class Animal { name = ‘animal’; constructor() { alert(this.name); // (*) } }

class Rabbit extends Animal { name = ‘rabbit’; }

new Animal(); // animal new Rabbit(); // animal

  1. 因为 `Rabbit` 中没有自己的构造器,所以 `Animal` 的构造器被调用了。<br />在这两种情况下:`new Animal()` `new Rabbit()`,在 `(*)` 行的 `alert` 都打印了 `animal`。<br />**换句话说, 父类构造器总是会使用它自己字段的值,而不是被重写的那一个。**
  2. 再看下面的代码, 下面调用 `this.showName()` 方法
  3. ```javascript
  4. class Animal {
  5. showName() { // 而不是 this.name = 'animal'
  6. alert('animal');
  7. }
  8. constructor() {
  9. this.showName(); // 而不是 alert(this.name);
  10. }
  11. }
  12. class Rabbit extends Animal {
  13. showName() {
  14. alert('rabbit');
  15. }
  16. }
  17. new Animal(); // animal
  18. new Rabbit(); // rabbit
  • 当父类构造器在派生的类中被调用时,它会使用被重写的方法。
  • 但对于类字段, 父类构造器总是使用父类的字段。


为什么呢
实际上,原因
在于字段初始化的顺序**。类字段是这样初始化的:

  • 对于基类(父类),在构造函数调用前初始化
  • 对于派生类在 **super()** 后立刻初始化

Rabbit 是派生类,里面没有 constructor()。相当于一个里面只有 super(...args) 的空构造器。

  • new Rabbit() 调用了 super(),因此它执行了父类构造器,根据派生类规则, 只有在此之后,它的类字段才被初始化
  • 在父类构造器被执行的时候,Rabbit 还没有自己的类字段,因此 Animal 类字段被使用了。

上述 constructor类字段 的重写都涉及 super, 下面深入研究super的原理
**

super的原理

super** 是如何工作的?
当一个对象方法执行时,它会将当前对象作为 this。如果调用 super.method(),那么引擎需要
从当前对象的原型**中获取 method。但这是怎么做到的?

在下面的例子中,rabbit.__proto__ = animal。如果在 rabbit.eat() 时, 将会使用 this.__proto__ 调用 animal.eat()

(*) 这一行,从原型(animal)中获取 eat,并在当前对象的上下文中调用它。
请注意.call(this) 在这里非常重要,因为简单的调用 this.__proto__.eat() 将在原型的上下文中执行 eat,而非当前对象。
在下面的代码中, 按照了期望运行:获得了正确的 alert

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.


在原型链上再添加一个对象**, 这件事将被打破的:

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() 时抛出了错误。
原因是 : 所有的对象方法都将当前对象作为 this,而非原型或其他什么东西。

  • (*)(**) 这两行中,this 的值都是当前对象(longEar)。


  • 因此,在 (*)(**) 这两行中,this.__proto__ 的值是完全相同的:都是 rabbit
    • 它们俩都调用的是 rabbit.eat,它们在不停地循环调用自己,而不是在原型链上向上寻找方法。

这张图介绍了发生的情况:
image.png

  1. longEar.eat() 中,(**) 这一行调用 rabbit.eat 并为其提供 this=longEar

    // 在 longEar.eat() 中我们有 this = longEar
    this.__proto__.eat.call(this) // (**)
    // 变成了
    longEar.__proto__.eat.call(this)
    // 也就是
    rabbit.eat.call(this);
    
  2. 之后在 rabbit.eat(*) 行中,我们希望将函数调用在原型链上向更高层传递,但是 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);
    
  3. ……所以 rabbit.eat 在不停地循环调用自己,因此它无法进一步地提升。

这个问题没法仅仅通过使用 **this** 来解决。
**

[[HomeObject]]

为了提供解决方法,JavaScript 为函数添加了一个特殊的内部属性:[[HomeObject]]

  1. 当一个函数被定义为类或者对象方法时,它的 [[HomeObject]] 属性就成为了该对象。
  2. 然后 super 使用它来解析父原型及其方法。

它是怎么工作的,首先,对于普通对象

  • 它基于 [[HomeObject]] 运行机制按照预期执行。一个方法,例如 longEar.eat,知道其 [[HomeObject]] 并且从其原型中获取父方法。并没有使用 this``javascript 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(); } };

// 正确执行 longEar.eat(); // Long Ear eats.

<a name="WvAhq"></a>
### 方法并不是“自由”的
函数通常都是“**自由**”的,并没有绑定到 JavaScript 中的对象。正因如此,它们**可以在对象之间复制,并用另外一个 `this` 调用它。**<br />`[[HomeObject]]` 的存在违反了这个原则,因为方法记住了它们的对象。**`[[HomeObject]]` 不能被更改,所以这个绑定是永久的。**<br />在 JavaScript 语言中 `[[HomeObject]]` 仅被用于 `super`。

<a name="ObMDo"></a>
### [方法,不是函数属性](https://zh.javascript.info/class-inheritance#fang-fa-bu-shi-han-shu-shu-xing)
`[[HomeObject]]` 是为**类和普通对象中的方法**定义的。但是对于对象而言,**方法必须确切指定为 ****`method()`****,而不是 ****`"method: function()"`**。<br />在下面的例子中,使用非方法(non-method)语法进行了比较。未设置 `[[HomeObject]]` 属性,并且继承无效:
```javascript
let animal = {
  eat: function() { // 这里是故意这样写的,而不是 eat() {...
    // ...
  }
};

let rabbit = {
  __proto__: animal,
  eat: function() {
    super.eat();
  }
};

rabbit.eat();  // 错误调用 super(因为这里没有 [[HomeObject]])