目录

🏀 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')
1
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 中的方法
1
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 中获取该方法。

image-20220516202917869

查找 run 方法的过程:

  • 查找对象 rabbit (没有 run )。
  • 查找它的原型,即 Rabbit.prototype (有 hide ,但没有 run )。
  • 查找它的原型,即(由于 extendsAnimal.prototype ,在这儿找到了 run 方法。

提示

extends 后允许任意表达式

🌰 例子:

function f(phrase) {
  return class {
    sayHi() { console.log(phrase); }
  };
}

class User extends f("Hello") {}

new User().sayHi(); // "Hello"
1
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()
  }
}
1
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!
1
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

Rabbitstop 方法中,调用父类的 super.stop() 方法,所以 Rabbit 也具有了父类的 stop 方法。

提示

** 箭头函数没有 super 。** 如果被访问,它会从外部函数获取。例如:

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // 1 秒后调用父类的 stop
  }
}
1
2
3
4
5

但是对于普通的函数,这里会获取不到外部的 super

setTimeout(function() { super.stop() }, 1000); // Error
1

# 重写 constructor

如果继承了父类的字类没有 constructor ,那么将生成下面这样的空 constructor

class Rabbit extends Animal {
  constructor(...args) {
    super(...args);
  }
}
1
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;
  }

  // ...
}
1
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
1
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 之前,必须在 Childconstructor 中将父 constructor 调用为 super()
    • 箭头函数没有自己的 thissuper ,它们从外部上下文获取 thissuper。
📢 上次更新: 2022/09/02, 10:18:16