总述
- 想要扩展一个类:
class Child extends Parent:- 这意味着
Child.prototype.__proto__将是Parent.prototype,所以方法会被继承。
- 这意味着
- 重写一个 constructor:
- 在使用
this之前,我们必须在Child的 constructor 中将父 constructor 调用为super()。
- 在使用
- 重写一个方法:
- 我们可以在一个
Child方法中使用super.method()来调用Parent方法。
- 我们可以在一个
- 内部:
- 方法在内部的
[[HomeObject]]属性中记住了它们的类/对象。这就是super如何解析父方法的。 - 因此,将一个带有
super的方法从一个对象复制到另一个对象是不安全的。 - 对于对象而言,方法必须确切指定为
method(),而不是"method: function()"extends关键字
关键字extends使用了很好的旧的原型机制进行工作。它将Rabbit.prototype.[[Prototype]]设置为Animal.prototype。
所以,如果在Rabbit.prototype中找不到一个方法,JavaScript 就会从Animal.prototype中获取该方法。
重写方法
在Rabbit中指定了自己的方法,例如stop(),那么将会使用它:
但通常来说,我们不希望完全替换父类的方法,而是希望在父类方法的基础上进行调整或扩展其功能。
- 方法在内部的
Class 为此提供了 "super" 关键字。
- 执行
super.method(...)来调用一个父类方法。 - 执行
super(...)来调用一个父类 constructor(只能在子类的constructor 中)。箭头函数没有 **
super**
// 父类Classclass Animal {// ...其他属性和方法stop() {this.speed = 0;alert(`${this.name} stands still.`);}}// 子类Classclass Rabbit extends Animal {hide() {alert(`${this.name} hides!`);}stop() {super.stop(); // 调用父类的 stopthis.hide(); // 然后 hide}}
重写 constructor
根据 规范,如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的“空” constructor:
- 它调用了父类的
constructor,在这个构造函数中,调用了super方法并传递了所有的参数class Rabbit extends Animal {// 为没有自己的 constructor 的扩展类生成的constructor(...args) {super(...args); //}}
但当给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.
<a name="3tCvR"></a>### constructor 里为什么必须调用 `super(...)`?在 JavaScript 中,继承类(所谓的“派生构造器”)的构造函数与其他函数之间是有区别的。- 派生构造器具有特殊的**内部属性** **`[[ConstructorKind]]:"derived"`**。这是一个特殊的内部标签。该标签会影响它的 `new` 行为:- 当通过 `new` 执行一个**常规函数**时,它将**创建一个空对象**,并将这个空对象赋值给 `this`。- 但当**继承的 constructor **执行时,它不会执行此操作。它**期望父类的 constructor 来完成这项工作。**因此,**派生的 constructor 必须调用 ****`super`**** 才能执行其父类(base)的 constructor,否则 ****`this`**** 指向的那个对象将不会被创建。并且会收到一个报错。**```javascriptclass Rabbit extends Animal {constructor(name, earLength) {super(name);this.earLength = earLength;}}// 现在可以了let rabbit = new Rabbit("White Rabbit", 10);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
因为 `Rabbit` 中没有自己的构造器,所以 `Animal` 的构造器被调用了。<br />在这两种情况下:`new Animal()` 和 `new Rabbit()`,在 `(*)` 行的 `alert` 都打印了 `animal`。<br />**换句话说, 父类构造器总是会使用它自己字段的值,而不是被重写的那一个。**再看下面的代码, 下面调用 `this.showName()` 方法```javascriptclass Animal {showName() { // 而不是 this.name = 'animal'alert('animal');}constructor() {this.showName(); // 而不是 alert(this.name);}}class Rabbit extends Animal {showName() {alert('rabbit');}}new Animal(); // animalnew 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,它们在不停地循环调用自己,而不是在原型链上向上寻找方法。
- 它们俩都调用的是
这张图介绍了发生的情况:
在
longEar.eat()中,(**)这一行调用rabbit.eat并为其提供this=longEar。// 在 longEar.eat() 中我们有 this = longEar this.__proto__.eat.call(this) // (**) // 变成了 longEar.__proto__.eat.call(this) // 也就是 rabbit.eat.call(this);之后在
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);……所以
rabbit.eat在不停地循环调用自己,因此它无法进一步地提升。
[[HomeObject]]
为了提供解决方法,JavaScript 为函数添加了一个特殊的内部属性:[[HomeObject]]。
- 当一个函数被定义为类或者对象方法时,它的
[[HomeObject]]属性就成为了该对象。 - 然后
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]])
