翻译 | JavaScript中的super()是什么含义?

背景
无意中看到一篇讲JavaScript中super关键字不错的文章,对文章进行翻译,仅供学习。 注:本人翻译水平有限,如果有错误,欢迎指正。 原文地址:What is super() in JavaScript? 原文作者:Bailey Jones 译文作者:Allen Gong
JavaScript中的super()是什么含义?(译文)
当你看到一些JavaScript代码在调用super()时,其中到底发生了什么?在子类中,你通过使用super()来调用父类的构造函数,调用super.<methodName>来访问父类的方法。这篇文章假设你至少对构造函数,子类父类这些概念有了基本的认识,如果你没有的话,你可能需要先阅读一下Mozilla的适合初学者的JavaScript面向对象文章。
Super不是JavaScript所特有的——很多编程语言,包括Java和Python,都有super()的关键字,它可以提供对父类的引用。与Java和Python不用,JavaScript不是以class模型构建的,取而代之的它是沿用JavaScript的原型继承模型的方式去实现与class模型相一致的行为特征。
让我们多了解一点并且看一些例子。
首先,以下对classes的定义,引用自Mozilla的官方文档:
ECMAScript 2015 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为JavaScript引入新的面向对象的继承模型。
下面是一个简单的子类继承父类的例子用来说明上面的引用的具体含义:
我的例子中有两个类:fish和trout。所有的fish都拥有habitat和length的信息,所有这些属性属于fish的类。Trout有一个variety的属性,它通过继承fish在最上面获得了这两个属性,下面是fish和trout的构造函数:
class fish {
constructor(habitat, length) {
this.habitat = habitat
this.length = length
}
}
class trout extends fish {
constructor(habitat, length, variety) {
super(habitat, length)
this.variety = variety
}
}
fish类的构造函数定义了habitat和length,trout的构造函数定义了variety。我必须在trout的构造函数中调用super(),否则我在尝试设置this.variety的时候会得到一个引用错误。那是因为我在trout类的第一行,我通过extentds关键字告诉JavaScript trout是fish的子类。那代表着trout的this上下文包含了fish类中定义的属性和方法,也包含trout自己定义的任何属性和方法。调用super()主要是为了让JavaScript知道fish是什么以便于它可以为trout创建this的上下文,包含所有在fish中定义的东西,也包含我们在trout中定义的。fish类不需要super()因为他的"父类"是JavaScript中的Object类。Fish已经是原型链中的顶端了,所有调用super()并不是非常必要——fish的this上下文只需要包含Object类,这一点JavaScript已经知道了。
对于fish和trout,在这条原型链上,所有的属性都是可访问的。从头开始,原型链从Object->fish->trout
我在trout的构造函数中调用super(habitat, length)(引用fish类),使得trout的this立刻拥有所有三个属性的访问权限。在trout的构造函数外面也有其他的方法可以获取同样的效果。为了避免引用错误,我必须调用super(),但是我不一定非要和fish的构造函数期望的一样的结构去调用它。那是因为我不需要通过使用super()来给fish创建的字段赋值——我只需要保证这些字段存在于trout的this上下文中即可。这是一个JavaScript与真正意义类模型的编程语言,如Java,最大的区别。下面的代码实现了fish类,但在这些语言中就会报错了:
class trout extends fish {
constructor(habitat, length, variety) {
super()
this.habitat = habitat
this.length = length
this.variety = variety
}
}
这种可供选择的trout的构造函数使我们更难分辨哪些属性属于fish和哪些属性属于trout,但是根据前面的例子获得的结果是一样的。唯一的区别在于调用没有参数的super()会创建一个this上下文,但是没有给habitat和length赋值。如果你在第三行调用console.log(this),它会打印出{habitat: undefined, length: undefined}。第四行和第五行赋值。
我也可以在trout构造函数外调用super(),目的是为了引用父类中的方法。这里我定义了一个renderProperties的方法,用来展示所有我传给HTML元素的class的属性。super()在这个时候就非常有用了因为我想我的trout类可以实现一个类似的方法但只要增加一点点东西——它在更新他的HTML之前赋值了一个类的名称。我可以通过调用super.renderProperties()复用fish类中的相关的类的方法。
class fish {
renderProperties(element) {
element.innerHTML = JSON.stringify(this)
}
}
class trout extends fish {
renderPropertiesWithSuper(element) {
element.className="green"
super.renderProperties(element);
}
函数名字的选择非常重要。我在trout类中的调用的方法的名字叫renderPropertiesWithSuper(),因为我还可以调用trout.renderProperties(),这个方法原来是在fish类中定义的。如果我将函数的名字定成renderProperties,那也是正确的。但是,但我再也无法通过trout的实例直接同时访问这两个函数——调用trout.renderProperties只会调用trout中定义的函数。这不是一个非常必要且有用的实现方式——一个函数覆盖父类函数的名字这样调用super,这是一个有争议的方式——但是这也论证了JavaScript允许你的class的灵活性。
不使用super()或者extends关键字实现这个例子,是完全有可能的,并且对于之前的例子是很有帮助的,只是不是那么方便。这就是Mozilla说的所谓"语法糖"的意思。事实上,如果我把之前的代码插入到类似Babel的转换器中,去保证旧的版本的我的类的代码和旧版JavaScript的代码兼容运行,这会产生类似下面的代码的东西。代码是相似的,但是你会发现没有extends和super(),我必须讲fish和trout定义成函数并且直接访问他们的原型对象。同样我也必须在15,16和17行做一些额外的操作用来让trout成为fish的子类,并且保证在trout的构造函数中被传入正确的this上下文。如果你对此感兴趣想深入了解到底原理是怎样的,Eric Green在博客中使用了很多例子来如何构建一个class。
在JavaScript中类是共享功能的一种强大的方式。举个例子,在React中的类组件中,是非常依赖类的。但是,如果你已经在其他面向对象编程语言中使用过类,你会发现JavaScript的行为还是有些让人惊讶。学习原型继承有时候会帮助理解JavaScript是如何与类协作的。