今天在和小伙伴聊天时, 发现对于 ES6 中的 Class 继承的理解我其实还是只是一个模棱两可的状态, 其实也不只是 constructor
有问题其它的部分其实也有问题, 这次先写类的继承之后再看有没有其它没理解的地方
整篇文章搭配食用 阮一峰老师的 ECMAScript 6 入门 - Class的继承 更佳
Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
Constructor 方法
首先, 在继承的类中 constructor
可以省略不写(会被默认添加)
class A {
echo () {
console.log('Hi!')
}
}
class B extends A {
}
let b = new B
b.echo() // Hi!
但是如果写了 constructor
就必须要在内部调用 super()
, 否则在创建实例的过程中会报错
class A {
echo () {
console.log('Hi!')
}
}
class B extends A {
constructor() {
}
}
let b = new B
b.echo()
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
从提示中就能看到, 必须调用 super()
才能完成塑造
阮一峰: 这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
从阮一峰老师的解释中也可以得出, 如果要在继承类中使用 this
就必须要先使用 super()
, 那么对应的属性声明也需要放到 super()
之后才行:
class A {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class B extends A {
constructor(x, y, z) {
this.z = z; // ReferenceError
super(x, y);
this.z = z; // Success
}
}
let b = new B
注意,super()
虽然代表了父类A的构造函数,但是返回的是子类B的实例,即 super()
内部的 this
指的是B的实例,因此 super()
在这里相当于 A.prototype.constructor.call(this)。
super
关键字
除了在 constructor
中被当作父类的构造函数使用( super()
),
也可以当成对象( super
)使用, 指向父类的原型对象, 相当于 A.prototype
, 但是在静态方法中 super
之中指向于父类
class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}
let b = new B();
这里需要注意,由于 super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super
调用的。
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
另外 ES6 规定,在子类普通方法中通过 super
调用父类的方法时,方法内部的 this
指向当前的子类实例。
所以通过 super
修改的是当前子类实例的属性,父类的属性并不会被修改
class A {
}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
super.x = 3;
console.log(super.x); // 2
console.log(this.x); // 3
}
}
let b = new B();
console.log(A.prototype.x) // 2
静态方法中的 super
虽然静态方法中的 super
就是按照字面的理解就行, 但是可能对于不是特别熟悉 Class
的朋友来说, 还是会有一点迷糊.那么就还是用阮一峰老师的例子来说
如果对于静态方法还不是特别清楚的, 请先阅读阮一峰老师的 ES6 入门 - Class 章节
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
let child = new Child();
child.myMethod(2); // instance 2
以上这个例子中, Child.myMethod(1)
是调用的调用的是 Child
类的静态方法 static
, 在静态方法中 super
指向于父类 Parent
并不是父类原型, 故输出的是 static 1
另外,在子类的静态方法中通过
super
调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
class A {
constructor() {
this.x = 1;
}
static print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
static m() {
super.print();
}
}
B.x = 3;
B.m() // 3
个人觉得这个例子还是好的, 如果不这样写, 可能就会有朋友会误以为会输出 2, 其实并不是, 这里的 this
指向的是 子类, 并不是 子类实例
🌰 举个对于初学者容易摸不着头脑的例子
我先把已知定义列出来:
- 在
constructor
函数中, 可以当作父类的构造函数使用 - 可以把
super
当成对象使用
- 普通方法 中
super
指向 父类的原型对象 - 静态方法 中
super
指向 父类- 静态方法 的
this
指向 当前子类
- 静态方法 的
Example #1
为什么可以赋值,但是读取的时候是 undefined
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
super
作为对象使用时提到过, 普通方法中 super
指向父类的原型对象,那么可以知道:
- 在输出
super.x
时, 读取到的其实是A.rototype.x
, 但是在父类原型链上并没有x
这个属性, 所以输出undefined
;
那为什么可以赋值呢?
我们先来看一下之前我故意跳过然后在这里讲的例子 👇
Example #2
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m_print() {
super.print();
}
}
let b = new B();
b.m() // 2
super.print()
虽然调用的是 A.prototype.print()
, 但是 A.prototype.print()
内部的 this
指向 子类B 的实例,导致输出的是 2 , 而不是 1 . 也就是说, 实际上执行的是 super.print.call(this)
所以在 Example #2
中, 由于 this
指向子类实例,赋值操作的时候属性会变成子类实例的属性, 所以修改的其实是 this.x
.
番外
在讨论的过程中我与小伙伴都有几个问题
constructor
中传递的参数是怎么决定的;
- 可以把
constructor
理解为调用父类构造函数, 这里传入的就是父类所需要的参数
- 如果只需要使用父类的几个方法呢;
- 继承的原意就是从父类的所有属性和方法, 如果不需要使用, 直接忽视就行了