前言
小伙伴们大家好,今天我们来说一下ES6中的super关键字,这次我尝试了一种新的写文形式:中英文结合,在本文的前半部分,我会引用 Bailey Jone 的一篇关于super的文章,后半部分我会结合ES6的标准和实际的小demo对其做一个总结,请看文章。
source:https://css-tricks.com/what-is-super-in-javascript/
Author:Bailey Jone
Published:Nov 6, 2019
What is super()?
What’s happening when you see some JavaScript that calls super()?.In a child class, you use super() to call its parent’s constructor and super.<methodName> to access its parent’s methods. This article will assume at least a little familiarity with the concepts of constructors and child and parent classes. If that’s totally new, you may want to start with Mozilla’s article on Object-oriented JavaScript for beginners.
Super isn’t unique to Javascript — many programming languages, including Java and Python, have a super() keyword that provides a reference to a parent class. JavaScript, unlike Java and Python, is not built around a class inheritance model. Instead, it extends JavaScript’s prototypal inheritance model to provide behavior that’s consistent with class inheritance.
Let’s learn a bit more about it and look at some code samples.
First off, here’s a quote from Mozilla’s web docs for classes:
JavaScript classes, introduced in ECMAScript 2015, are primarily syntactical sugar over JavaScript’s existing prototype-based inheritance. The class syntax does not introduce a new object-oriented inheritance model to JavaScript.
An example of a simple child and parent class will help illustrate what this quote really means:
<!--HTML代码--> <h4>grouper</h4> <div id="grouper"></div> <h4>rainbow trout</h4> <div id="rainbowTrout"></div> <div id="rainbowTroutParent"></div>
/*CSS样式*/ .green { color: green; }
/*js代码*/ class Fish { constructor(habitat, length) { this.habitat = habitat this.length = length } renderProperties(element) { element.innerHTML = JSON.stringify(this) } } class Trout extends Fish { constructor(habitat, length, variety) { super(habitat, length) this.variety = variety } renderPropertiesWithSuper(element) { element.className="green" super.renderProperties(element); } } let grouper = new Fish("saltwater", "26in"); console.log(grouper); grouper.renderProperties(document.getElementById("grouper")); let rainbowTrout = new Trout("freshwater", "14in", "rainbow"); console.log(rainbowTrout); //invoke function from parent prototype rainbowTrout.renderProperties(document.getElementById("rainbowTrout")); //invoke function from child's prototype rainbowTrout.renderPropertiesWithSuper(document.getElementById("rainbowTroutParent"));
My example has two classes: fish and trout. All fish have information for habitat and length, so those properties belong to the fish class. Trout also has a property for variety, so it extends fish to build on top of the other two properties. Here are the constructors for fish and trout:
/*js代码*/ 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 } }
The fish class’s constructor defines habitat and length, and the trout’s constructor defines variety. I have to call super() in trout’s constructor, or I’ll get a reference error when I try to set this.variety. That’s because on line one of the trout class, I told JavaScript that trout is a child of fish using the extends keyword. That means trout’s this context includes the properties and methods defined in the fish class, plus whatever properties and methods trout defines for itself. Calling super() essentially lets JavaScript know what a fish is so that it can create a this context for trout that includes everything from fish, plus everything we’re about to define for trout. The fish class doesn’t need super() because its “parent” is just the JavaScript Object. Fish is already at the top of the prototypal inheritance chain, so calling super() is not necessary — fish’s this context only needs to include Object, which JavaScript already knows about.
The prototypal inheritance model for fish and trout and the properties available on the this context for each of them. Starting from the top, the prototypal inheritance chain here goes Object
→ fish
→ trout
.
I called super(habitat, length) in trout’s constructor (referencing the fish class), making all three properties immediately available on the this context for trout. There’s actually another way to get the same behavior out of trout’s constructor. I must call super() to avoid a reference error, but I don’t have to call it “correctly” with parameters that fish’s constructor expects. That’s because I don’t have to use super() to assign values to the fields that fish creates — I just have to make sure that those fields exist on trout’s this context. This is an important difference between JavaScript and a true class inheritance model, like Java, where the following code could be illegal depending on how I implemented the fish class:
/*js代码*/ class trout extends fish { constructor(habitat, length, variety) { super() this.habitat = habitat this.length = length this.variety = variety } }
This alternate trout constructor makes it harder to tell which properties belong to fish and which belong to trout, but it gets the same result as the previous example. The only difference is that here, calling super() with no parameters creates the properties habitat and length on the current this context without assigning anything to them. If I called console.log(this) after line three, it would print {habitat: undefined, length: undefined}. Lines four and five assign values.
I can also use super() outside of trout’s constructor in order to reference methods on the parent class. Here I’ve defined a renderProperties method that will display all the class’s properties into the HTML element I pass to it. super() is useful here because I want my trout class to implement a similar method that does the same thing plus a little more — it assigns a class name to the element before updating its HTML. I can reuse the logic from the fish class by calling super.renderProperties() inside the relevant class function.
/*js代码*/ class fish { renderProperties(element) { element.innerHTML = JSON.stringify(this) } } class trout extends fish { renderPropertiesWithSuper(element) { element.className="green" super.renderProperties(element); }
The name you choose is important. I’ve called my method in the trout class renderPropertiesWithSuper() because I still want to have the option of calling trout.renderProperties() as it’s defined on the fish class. If I’d just named the function in the trout class renderProperties, that would be perfectly valid; however, I’d no longer be able to access both functions directly from an instance of trout – calling trout.renderProperties would call the function defined on trout. This isn’t necessarily a useful implementation – it’s an arguably better pattern for a function that calls super like this to overwrite its parent function’s name – but it does illustrate how flexible JavaScript allows your classes to be.
It is completely possible to implement this example without the use of the super() or extends keywords that were so helpful in the previous code sample, it’s just much less convenient. That’s what Mozilla meant by “syntactical sugar.” In fact, if I plugged my previous code into a transpiler like Babel to make sure my classes worked with older versions of JavaScript, it would generate something closer to the following code. The code here is mostly the same, but you’ll notice that without extends and super(), I have to define fish and trout as functions and access their prototypes directly. I also have to do some extra wiring on lines 15, 16, and 17 to establish trout as a child of fish and to make sure trout can be passed the correct this context in its constructor. If you’re interested in a deep dive into what’s going on here, Eric Green has an excellent post with lots of code samples on how to build classes with and without ES2015.
/*js代码-ES2015*/ function Fish(habitat, length) { this.habitat = habitat; this.length = length; } Fish.prototype.renderProperties = function(element) { element.innerHTML = JSON.stringify(this) }; function Trout(habitat, length, variety) { this._super.call(this, habitat, length); this.variety = variety; } Trout.prototype = Object.create(Fish.prototype); Trout.prototype.constructor = Trout; Trout.prototype._super = Fish; Trout.prototype.renderPropertiesWithSuper = function(element) { element.className="green"; this.renderProperties(element); }; let grouper = new Fish("saltwater", "26in"); grouper.renderProperties(document.getElementById("grouper")); var rainbowTrout = new Trout("freshwater", "14in", "rainbow"); //invoke function from parent rainbowTrout.renderProperties(document.getElementById("rainbowTrout")); //invoke function from child rainbowTrout.renderPropertiesWithSuper(document.getElementById("rainbowTroutParent"));```
Classes in JavaScript are a powerful way to share functionality. Class components in React, for example, rely on them. However, if you’re used to Object Oriented programming in another language that uses a class inheritance model, JavaScript’s behavior can occasionally be surprising. Learning the basics of prototypal inheritance can help clarify how to work with classes in JavaScript.
说一下super
super 这个关键字既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的使用方法完全不同。
01 - 作为函数使用
该情况下,super作为函数调用时代表父类的构造函数。在ES6标准之下,子类的构造函数必须执行一次super函数,请看演示代码:
/*js代码*/ class Family{ constructor(name,age) { this.name = name; this.age = age; } loveFamily(){ console.log(`我是父类中的方法:We are a big family,and her name is ${this.name},she has been ${this.age} years old,all our members love it deeply.`) } } class Child extends Family{ constructor(name,age,mother) { super(name,age); this.mother = mother; } loveMom(){ console.log(`我是子类中的方法:My name is ${this.name},i am ${this.age} years old,and this is my mom,her name is ${this.mother}.`); } } let family = new Family("hahaCoder",100) family.loveFamily() let child = new Child("shipudong",22,"Hui") child.loveMom()
上述代码中,我们定义了父类 Family ,子类 Child,并让子类通过super关键字继承了来自父类的两个属性:name和age,如果在子类中我们没有调用父类的构造函数,即在子类中没有使用super关键字,JS引擎就会报错:
我们知道,super表示父类 Family 的构造函数,但是当在子类 Child 中使用的时候,其返回的是子类的实例,即其内部this指针指向的是子类Child,因此我们可以这样理解:
super() === Family.prototype.constructor.call(this)
上述代码太过冗余了,我们不看它了,来写一小段代码,证实上述的这个结论:
/*js代码*/ class A { constructor() { console.log(`亲,当前正在执行的是${new.target.name}函数哟~`) } } class B extends A{ constructor() { super(); } } let a_exp = new A() let b_exp = new B()
上述代码中,new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数,也就是说super()内部的this指向的是B,故上述结论得证。
最后,关于当super作为一个函数使用时的情况,我们在提醒最后一点:super()只能用在子类的构造函数中,用在其他地方会报错,请看错误代码:
/*js代码*/ class A {} class B extends A{ m(){ super(); } }
02 - 作为对象使用
该情况下,super作为对象时在普通方法中指向父类的原型对象;在静态方法中指向父类。
我们来看一段演示代码:
/*js代码*/ class SuperType { constructor(name,age) { this.name = name; this.age = age; } sayHello(){ console.log(`Hello,this is ${this.name},he is ${this.age} years old,nice to meet you guys!`) } } class SubType extends SuperType{ constructor() { super(); super.sayHello(); console.log(super.name); } } let sub = new SubType()
我们知道,在ES6中如果我们需要在类的原型对象上定义方法,可以直接在class中去写,不用在像以前这样:
/*js代码*/ function Person(){} Person.prototype.sayName = function(){ /* 逻辑功能 */ }
因为在ES6标准中,方法是直接定义在原型对象上的,因此子类的实例对象可以借助原型链访问到子类的原型对象、父类的原型对象上的所有方法,但当我们想要访问父类实例中的属性时,却不能访问到,这是因为super指向父类的原型对象,定义在父类实例上的方法或属性无法通过super来调用。上述代码中通过super关键字调用父类的方法这一行代码可以等价为如下代码:
/*js代码*/ class SubType extends SuperType{ constructor() { super(); SuperType.prototype.sayHello(); // 本行代码等价于super.sayHello(); } }
如果我们必须要访问父类实例上的属性或方法,那我们就必须做如下定义:
/*js代码*/ class A{} A.prototype.x = 2; class B extends A{ constructor(){ super(); console.log(super.x); } }
即把我们想要访问到的属性和方法,定义在父类的原型对象上。
在ES6标准下,规定:当通过super()调用父类的方法时,super会被绑定到子类的this,我们来看一段演示代码:
/*js代码*/ class SuperType { constructor() { this.x = 1; } } class SubType extends SuperType{ constructor() { super(); this.x = 2; super.x = 3; console.log(super.x); // undefined console.log(this.x); // 3 } } let sub = new SubType()
上述代码中,由于super会绑定子类的this,因此当通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性,故当super.x被赋值为3时,等同于对this.x赋值为3,故当读取this.x时候,其值为3,当读取super.x时,由于其父类原型对象上并没有关于x的任何定义,故其值为undefined。
我们最后再来说一下super在静态方法中的情况:
首先,我们有必要来说一下class中的静态方法:我们知道,类相当于实例的原型,所有类中定义的方法都会被实例继承,如果在一个方法前加上static关键字,就表示该方法不会被实例继承,而是直接通过类调用。
接下来, 我们看一段代码:
/*js代码*/ class SuperType { static SuperMethod(info){ console.log(`我是父类中的静态方法,我被定义在父类中:${info}`) } SuperMethod(info){ console.log(`我是父类中的普通方法,我被定义在父类的原型对象上:${info}`) } } class SubType extends SuperType{ static SuperMethod(info){ super.SuperMethod(info) } SuperMethod(info){ super.SuperMethod(info) } } SubType.SuperMethod("hello,静态方法") let sub = new SubType() sub.SuperMethod("hello,普通方法")
上述代码中,super在静态方法中指向父类,在普通方法中执向父类的原型对象。
最后,在提醒各位小伙伴两点:
- 使用super时,必须显示的指定super是作为函数使用还是作为对象使用
/*js代码*/ class A {} class B extends A{ constructor() { super(); console.log(super); } }
- 任意一个对象均可使用super关键字,因为对象总是继承于其他对象的
/*js代码*/ let obj = { sayName(){ console.log( `sayName:${super.toString()}`) } } obj.sayName()
写在最后
本文我们主要讲解了super关键字的两种应用场景,包括在函数中使用和在对象中使用,并尝试了一种新的写文方式:中英结合,小伙伴们理解了吗?快去实现一下吧~