🏀 JavaScript 类继承
继承可以 通过一个类 拓展另一个类。
# extends
🌰 例子:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
console.log(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
cnosole.log(`${this.name} stands still.`);
}
}
let animal = new Animal('animal')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
要创建另一个 继承 Animal
的类 rabbit
,使用 extends
关键字,可以访问 animal
的方法,以便 Rabbit
类可以做「一般」动物可以做的事。
class Rabbit extends Animal {
hide() {
console.log(`${this.name} hides!`)
}
}
let rabbit = new Rabbit('white rabbit')
rabbit.run(5) // 访问 Animal 中的方法
rabbit.hide // 访问 Rabbit 中的方法
2
3
4
5
6
7
8
9
Class Rabbit
的对象可以访问例如 rabbit.hide()
等 Rabbit
的方法,还可以访问例如 rabbit.run()
等 Animal
的方法。
在内部,关键字
extends
使用了很好的旧的 原型机制 进行工作。它将Rabbit.prototype.[[Prototype]]
设置为Animal.prototype
。所以,如果在Rabbit.prototype
中找不到一个方法,JavaScript 就会从Animal.prototype
中获取该方法。查找
run
方法的过程:
- 查找对象
rabbit
(没有run
)。- 查找它的原型,即
Rabbit.prototype
(有hide
,但没有run
)。- 查找它的原型,即(由于
extends
)Animal.prototype
,在这儿找到了run
方法。
提示
在 extends
后允许任意表达式。
🌰 例子:
function f(phrase) {
return class {
sayHi() { console.log(phrase); }
};
}
class User extends f("Hello") {}
new User().sayHi(); // "Hello"
2
3
4
5
6
7
8
9
这里
class User
继承自f("Hello")
的结果。
对于高级编程模式,例如当 根据许多条件使用函数生成类,并继承它们时来说可能很有用。
# 重写方法
🌰 例子 / Rabbit
继承 Animal
的例子中,重写 stop()
:
class Rabbit extends Animal {
stop() {
// ……现在这个将会被用作 rabbit.stop()
// 而不是来自于 class Animal 的 stop()
}
}
2
3
4
5
6
通常不会完全重写父类的方法,而是 在父类的基础上调整或者拓展。
可以使用 super
关键字
- 执行
super.method(...)
来调用一个父类方法。 - 执行
super(...)
来调用一个父类constructor
(只能在constructor
中调用)。
🌰 例子:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
console.log(`${this.name} runs with speed ${this.speed}.`);
}
stop() {
this.speed = 0;
console.log(`${this.name} stands still.`);
}
}
class Rabbit extends Animal {
hide() {
console.log(`${this.name} hides!`);
}
stop() {
super.stop(); // 调用父类的 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!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
在
Rabbit
的stop
方法中,调用父类的super.stop()
方法,所以Rabbit
也具有了父类的stop
方法。
提示
** 箭头函数没有 super
。** 如果被访问,它会从外部函数获取。例如:
class Rabbit extends Animal {
stop() {
setTimeout(() => super.stop(), 1000); // 1 秒后调用父类的 stop
}
}
2
3
4
5
但是对于普通的函数,这里会获取不到外部的 super
:
setTimeout(function() { super.stop() }, 1000); // Error
# 重写 constructor
如果继承了父类的字类没有 constructor
,那么将生成下面这样的空 constructor
:
class Rabbit extends Animal {
constructor(...args) {
super(...args);
}
}
2
3
4
5
如果要添加 子类的 一个自定义的 constructor
,继承类的 constructor
必须调用 super(...)
,并且 一定要在使用 this
之前调用。
在 JavaScript 中,继承类的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性
[[ConstructorKind]]:"derived"
,这是一个特殊的内部标签。该标签会影响它的new
行为:
- 当通过
new
执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给this
。- 但是当继承的
constructor
执行时,它不会执行此操作。它期望 父类的constructor
来完成这项工作。因此,派生的
constructor
必须调用super
才能执行其 父类的constructor
,否则this
指向的那个对象将不会被创建。
🌰 例子:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
class Rabbit extends Animal {
constructor(name, earLength) {
super(name);
this.earLength = earLength;
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 重写类字段
🌰 例子 / 当访问 被重写的字段时:
class Animal {
name = 'animal';
constructor() {
alert(this.name); // (*)
}
}
class Rabbit extends Animal {
name = 'rabbit';
}
new Animal(); // animal
new Rabbit(); // animal
2
3
4
5
6
7
8
9
10
11
12
13
14
Rabbit
继承自Animal
,并且用它自己的值重写了name
字段。因为
Rabbit
中没有自己的构造器,所以Animal
的构造器被调用了。这两种情况下,都打印了animal
。换句话说,父类构造器总是会使用它自己字段的值,而不是被重写的那一个。
当父类构造器在派生的类中被调用时,它会使用被重写的方法。但对于类字段并非如此。正如前文所述,父类构造器总是使用父类的字段。
实际上,原因在于字段初始化的顺序。类字段是这样初始化的:
- 对于基类(还未继承任何东西的那种),在构造函数调用前初始化。
- 对于派生类,在
super()
后立刻初始化。
上面的例子中, Rabbit
是派生类,里面没有 constructor()
。正如先前所说,这相当于一个里面只有 super(...args)
的空构造器。
所以, new Rabbit()
调用了 super()
,因此它执行了父类构造器,并且(根据派生类规则)只有在此之后,它的类字段才被初始化。在父类构造器被执行的时候, Rabbit
还没有自己的类字段,这就是为什么 Animal
类字段被使用了。
这种字段与方法之间微妙的区别只特定于 JavaScript。这种行为仅在一个被重写的字段被父类构造器使用时才会显现出来。
# 深入 内部探究 和 [[HomeObject]]
关于继承和 super
背后的内部机制。
# 总结
想要扩展一个类:
class Child extends Parent
- 这意味着
Child.prototype.__proto__
将是Parent.prototype
,所以方法会被继承。
- 这意味着
重写一个方法:
- 可以在一个
Child
方法中使用super.method()
来调用Parent
方法
- 可以在一个
重写一个
constructor
:- 在使用
this
之前,必须在Child
的constructor
中将父constructor
调用为super()
。
- 箭头函数没有自己的
this
或super
,它们从外部上下文获取this
和super。
- 在使用