作者:Anurag Majumdar 原文链接:“Super” and “Extends” In JavaScript ES6 - Understanding The Tough Parts
ES6 的 class 语法以及其他新特性让 JavaScript 显得简单多了。今天我们将结合 class 语法和继承 概念来分析一些代码。猜的没错,我们要研究的正是 ES6 中的 super 和 extends 关键字。学习新特性最好的方式就是在实例中深入的研究它。让我们开始吧!
在代码中使用 super 和 extends 我们可以借助 super 和 extends 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());
ES6 Class 及 Subclass 语法
上面的代码中有两个 JavaScript class,分别叫做 Animal 和 Gorilla 。
Gorilla class 通过使用 extends 关键字将自己设置为 Animal class 的附属类, 或者也可以称之为子类 。
不过,不知道你有没有注意到,super 关键字在这里有两种不同的用法。在 Gorilla 构造函数中(代码 23 行),super 被当作方法使用,然而在 Gorilla 的 showVigour() 方法(代码 35 行)及 dailyRoutine() 方法(代码 39 行)中 super 却是被当作“对象” 使用的。
super 关键字有两种不同用法的原因是:在第 23 行中,super 关键字是作为一个函数 ,它使用传递给 Gorilla 的参数来调用父类 Animal 。这是确保 Gorilla 是 Animal 的实例的关键一步。在第 35 行和第 39 行中,Super 被用作是一个对象 ,来引用一个 Animal 实例(父类)。这里的 super 关键字用来显式的调用父类 Animal 的方法。
熟悉 C#、JAVA、Python 等语言的人应该能明白这是怎么回事。不过,在 ES6 出现之前 JavaScript 可没这么简明,特别是在类这个问题上。那么在没有 class 语法、super 及 extends 关键字的年代人们是怎么写代码的呢?还是这群从未用过以上概念的人突发奇想的要添加这些特性?让我们来探寻一下!
传统的 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());
ES6 出现之前面向对象的 JavaScript
看完上面的代码以后你们一定在想,等一下,这里面怎么没有 class ?也没有 constructor ?这些古老的 JavaScript 代码是如何做到在不使用 extends 和 super 关键字的情况下实现继承的?这样写代码不是很丑嘛?
没错儿我完全理解你们的感受。很不幸,JavaScript 的底层实现从未变过。不管有多少新特性被添加,它的底层仍然是老样子。class 、constructor 、super 和 extends 等关键字只是为他添加了一些语法糖,以使得代码可读性更好也更易于编写。
让我来解释一下 ES6 哪些代码对应了传统的 JavaScript 代码。
如果你是初次接触 JavaScript 中的 prototype 和 inheritance 概念,请先阅读下面两篇文章后再继续阅读本文剩下的章节:
这两篇参考文章对你更好的理解下面的章节会有很大帮助。
ES6 与传统 JavaScript 代码对比 下面的章节回分解对比 ES6 代码与传统 JavaScript 风格代码的异同。
类声明 下面的代码片段对比了两种类声明方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Animal { constructor (name, weight) { this .name = name; this .weight = weight; } } typeof Animal 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 class Animal { eat() { return `${this .name} is eating!` ; } sleep() { return `${this .name} is going to sleep!` ; } wakeUp() { return `${this .name} is waking up!` ; } } 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 class Gorilla extends Animal { constructor (name, weight) { super (name, weight); } } 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 设置为一个从 Animal 的 prototype 创建的新对象,如代码第 11 行所示。这样一来,我们就重写了 Gorilla 的 prototype 对象。所以,我们需要在代码第 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 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()} ` ; } } 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 代码。
祝编码快乐!😄