关于 ES6 Class 继承中 Constructor 的整理

今天在和小伙伴聊天时, 发现对于 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 指向的是 子类, 并不是 子类实例

🌰 举个对于初学者容易摸不着头脑的例子


我先把已知定义列出来:

  1. constructor 函数中, 可以当作父类的构造函数使用
  2. 可以把 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.

番外

在讨论的过程中我与小伙伴都有几个问题

  1. constructor 中传递的参数是怎么决定的;
  • 可以把 constructor 理解为调用父类构造函数, 这里传入的就是父类所需要的参数
  1. 如果只需要使用父类的几个方法呢;
  • 继承的原意就是从父类的所有属性和方法, 如果不需要使用, 直接忽视就行了