「译」深度理解 JavaScript ES6 中的 “Super” 和 “Extends”

作者:Anurag Majumdar
原文链接:“Super” and “Extends” In JavaScript ES6 - Understanding The Tough Parts

ES6class 语法以及其他新特性让 JavaScript 显得简单多了。今天我们将结合 class 语法和继承概念来分析一些代码。猜的没错,我们要研究的正是 ES6 中的 superextends 关键字。学习新特性最好的方式就是在实例中深入的研究它。让我们开始吧!

在代码中使用 super 和 extends

我们可以借助 superextends JavaScript 中扩展一个 class。来看一下下面的例子中是如何使用这两个关键字的。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Animal {
constructor(name, weight) {
this.name = name;
this.weight = weight;
}

eat() {
return `${this.name} is eating!`;
}

sleep() {
return `${this.name} is going to sleep!`;
}

wakeUp() {
return `${this.name} is waking up!`;
}

}

class Gorilla extends Animal {
constructor(name, weight) {
super(name, weight);
}

climbTrees() {
return `${this.name} is climbing trees!`;
}

poundChest() {
return `${this.name} is pounding its chest!`;
}

showVigour() {
return `${super.eat()} ${this.poundChest()}`;
}

dailyRoutine() {
return `${super.wakeUp()} ${this.poundChest()} ${super.eat()} ${super.sleep()}`;
}

}

function display(content) {
console.log(content);
}

const gorilla = new Gorilla('George', '160Kg');
display(gorilla.poundChest());
display(gorilla.sleep());
display(gorilla.showVigour());
display(gorilla.dailyRoutine());

// OUTPUT:
// George is pounding its chest!
// George is going to sleep!
// George is eating! George is pounding its chest!
// George is waking up! George is pounding its chest! George is eating! George is going to sleep!

ES6 Class 及 Subclass 语法

上面的代码中有两个 JavaScript class,分别叫做 AnimalGorilla

Gorilla class 通过使用 extends 关键字将自己设置为 Animal class 的附属类,或者也可以称之为子类

不过,不知道你有没有注意到,super 关键字在这里有两种不同的用法。在 Gorilla 构造函数中(代码 23 行),super 被当作方法使用,然而在 GorillashowVigour() 方法(代码 35 行)及 dailyRoutine() 方法(代码 39 行)中 super 却是被当作“对象”使用的。

super 关键字有两种不同用法的原因是:在第 23 行中,super 关键字是作为一个函数,它使用传递给 Gorilla 的参数来调用父类 Animal。这是确保 GorillaAnimal 的实例的关键一步。在第 35 行和第 39 行中,Super 被用作是一个对象,来引用一个 Animal 实例(父类)。这里的 super 关键字用来显式的调用父类 Animal 的方法。

熟悉 C#、JAVA、Python 等语言的人应该能明白这是怎么回事。不过,在 ES6 出现之前 JavaScript 可没这么简明,特别是在类这个问题上。那么在没有 class 语法、superextends 关键字的年代人们是怎么写代码的呢?还是这群从未用过以上概念的人突发奇想的要添加这些特性?让我们来探寻一下!

传统的 JavaScript 类

事实上,面向对象的 JavaScript 使用原型继承来扩展类。我们来看一下用传统的 JavaScript 语法来写上面的例子。也许这将帮你找到隐藏的真相。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function Animal(name, weight) {
this.name = name;
this.weight = weight;
}

Animal.prototype.eat = function() {
return `${this.name} is eating!`;
}

Animal.prototype.sleep = function() {
return `${this.name} is going to sleep!`;
}

Animal.prototype.wakeUp = function() {
return `${this.name} is waking up!`;
}


function Gorilla(name, weight) {
Animal.call(this, name, weight);
}

Gorilla.prototype = Object.create(Animal.prototype);
Gorilla.prototype.constructor = Gorilla;

Gorilla.prototype.climbTrees = function () {
return `${this.name} is climbing trees!`;
}

Gorilla.prototype.poundChest = function() {
return `${this.name} is pounding its chest!`;
}

Gorilla.prototype.showVigour = function () {
return `${Animal.prototype.eat.call(this)} ${this.poundChest()}`;
}

Gorilla.prototype.dailyRoutine = function() {
return `${Animal.prototype.wakeUp.call(this)} ${this.poundChest()} ${Animal.prototype.eat.call(this)} ${Animal.prototype.sleep.call(this)}`;
}

function display(content) {
console.log(content);
}

var gorilla = new Gorilla('George', '160Kg');
display(gorilla.poundChest());
display(gorilla.sleep());
display(gorilla.showVigour());
display(gorilla.dailyRoutine());

// OUTPUT:
// George is pounding its chest!
// George is going to sleep!
// George is eating! George is pounding its chest!
// George is waking up! George is pounding its chest! George is eating! George is going to sleep!

ES6 出现之前面向对象的 JavaScript

看完上面的代码以后你们一定在想,等一下,这里面怎么没有 class?也没有 constructor?这些古老的 JavaScript 代码是如何做到在不使用 extendssuper 关键字的情况下实现继承的?这样写代码不是很丑嘛?

没错儿我完全理解你们的感受。很不幸,JavaScript 的底层实现从未变过。不管有多少新特性被添加,它的底层仍然是老样子。classconstructorsuperextends 等关键字只是为他添加了一些语法糖,以使得代码可读性更好也更易于编写。

让我来解释一下 ES6 哪些代码对应了传统的 JavaScript 代码。

如果你是初次接触 JavaScript 中的 prototypeinheritance 概念,请先阅读下面两篇文章后再继续阅读本文剩下的章节:

这两篇参考文章对你更好的理解下面的章节会有很大帮助。

ES6 与传统 JavaScript 代码对比

下面的章节回分解对比 ES6 代码与传统 JavaScript 风格代码的异同。

类声明

下面的代码片段对比了两种类声明方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ES6 style
class Animal {
constructor(name, weight) {
this.name = name;
this.weight = weight;
}
//...
}

// Check Type of ES6 class
typeof Animal // function

// Traditional style
function Animal(name, weight) {
this.name = name;
this.weight = weight;
}

类声明对比

ES6 的类声明直接使用 class 关键字,然后在 constructor (构造函数)中定义实例变量。而在传统的 JavaScript 中是没有 class 这种东西的。实际上在 JavaScript 内部 class 就是一个函数(参见代码片段第 11 行)。

第 3 行的 constructor 函数跟第 14 行的函数是一样的。这里的 Animal 函数就相当于 constructor 函数。

Class 中的方法

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
// ES6 style
class Animal {
// ...
eat() {
return `${this.name} is eating!`;
}

sleep() {
return `${this.name} is going to sleep!`;
}

wakeUp() {
return `${this.name} is waking up!`;
}
// ...
}

// Traditional style
Animal.prototype.eat = function() {
return `${this.name} is eating!`;
}

Animal.prototype.sleep = function() {
return `${this.name} is going to sleep!`;
}

方法声明对比

从第 4 行到第 14 行是 ES6 风格的类中的方法。不过在传统的 JavaScript 是不可能这样写的,因为根本就不存在 class 这个东西来让你轻松的声明方法。在传统的 JavaScript 中,我们通过把方法添加到 prototype 上来达到为类添加方法这一目的。第 19 行到第 29 行是为传统 JavaScript 类添加方法的写法。

extends 在传统 JavaScript 中的对应

当我们要从父类扩展一个子类时,两种写法的差异就更大了。参见下面的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ES6 style
class Gorilla extends Animal {
constructor(name, weight) {
super(name, weight);
}
//...
}

// Traditional style
function Gorilla(name, weight) {
Animal.call(this, name, weight);
}

Gorilla.prototype = Object.create(Animal.prototype);
Gorilla.prototype.constructor = Gorilla;

extends 关键字与传统实现方式的对比

我们看到 extends 关键字用 ES6 的风格处理了从父类 Animal 扩展出子类的操作,不过这里也用到了 super 关键字来确保 Gorilla 的构造函数调用了 Animal 类,这样它就能继承 Animal 的特性和行为。在这里,super 关键字是作为函数来调用 Animal 类来初始化 Gorilla。这时候,super 的作用相当于 Animal.call(this,…)

若是用传统的写法来实现继承,则要多几个步骤。首先要创建一个子类 Gorilla 的函数,如代码第 10 行所示。由于 Gorilla 要继承 Animal 的特性和行为,它需要在 Gorilla 的构造函数中调用 Animal 的构造函数,如代码第 11 行所示,这行与第 4 行的功能是一样的。只不过我们需要显式的传递 “this” 引用给 Animal 类以保证调用是在 Gorilla 中发生的。

不仅如此,我们还需要将 Gorilla 函数的 prototype 设置为一个从 Animalprototype 创建的新对象,如代码第 11 行所示。这样一来,我们就重写了 Gorillaprototype 对象。所以,我们需要在代码第 15 行中显式的设置 Gorilla 的构造函数。通过这些步骤我们将 Gorilla 类设置成为 Animal 类的子类。

super 在传统 JavaScript 中的对应

我们已经见识过 super 关键字的一种对应,即,下面代码片段第 4 行和第 19 行,super 被用作函数

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
// ES6 style
class Gorilla extends Animal {
constructor(name, weight) {
super(name, weight);
}

showVigour() {
return `${super.eat()} ${this.poundChest()}`;
}

dailyRoutine() {
return `${super.wakeUp()} ${this.poundChest()} ${super.eat()} ${super.sleep()}`;
}
// ...
}

// Traditional style
function Gorilla(name, weight) {
Animal.call(this, name, weight);
}

Gorilla.prototype = Object.create(Animal.prototype);
Gorilla.prototype.constructor = Gorilla;

Gorilla.prototype.showVigour = function () {
return `${Animal.prototype.eat.call(this)} ${this.poundChest()}`;
}

Gorilla.prototype.dailyRoutine = function() {

super 关键字与传统实现方式的对比

super 关键字也可以用作父类的实例,如代码第 8 行和第 12 行这样调用 Animal 类的方法。

代码第 26 行展示了在传统的 JavaScript 中实现同样的目的,super 实例相当于传统写法的 ParentClassName.prototype.methodName.call(this, …)。即传统写法需要编写大量的代码来确保父类的方法被调用。

结论

我很确定,你们在了解了传统的写法是多么的复杂之后一定会毫不犹豫的选择 ES6继承特性。而且,Chrome 和 Firefox 已经支持了 ES6,不过为了确保 ES6 特性在所有的浏览器中都能执行,你还是需要一个 babel 转译器来把 ES6 代码转换成 ES5 代码。

祝编码快乐!😄

「译」理解 JavaScript 中的 Promise 「译」Web Components:入门到精通 (1)
广告: