在ES6之前,实现继承不是一个容易的操作,我们需要先建立一个子类的实例对象this,然后再将父类的方法添加到这个this上面(Parent.apply(this))来实现继承。

我们可以先来看看ES5继承的实现方式,请看下面示例:

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
function Rectangle (length, width) {
this.length = length;
this.width = width;
}

Rectangle.prototype.getArea = function () {
return this.length * this.width
}

function Square (width, length) {
Rectangle.call(this, width, length)
}

Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
writable: true,
enumerable: true,
configurable: true
}
});

let square = new Square(2, 3);
console.log(square.getArea()) // 6
console.log(square instanceof Square) // true
console.log(square instanceof Rectangle) // true

以上代码简单的实现了一个继承过程。为了实现这个继承过程,我们先创建了子类对象Square的实例对象this,然后调用call方法将父类Reatangle的方法添加到子类对象的this上。

ES6中的继承

学习过其他面向对象编程语言的都或多或少知道类和类继承的概念,而JavaScript虽然也是面向对象的却并不支持这些特性,要实现类的继承只能通过其他方法定义并关联上多个相似的对象。这个状态一直从ECMAScript1延续到了ECMAScript5, 直到ECMAScript6终于引入了类的概念。
ES6类的出现让我们可以轻松的实现继承功能,和其他能够实现类继承的编程语言一样,ES6的继承也是通过关键字extends来实现的。ES6实现继承的实质是先创造父类的实例对象this,然后通过子类的构造函数修改this。
下面我们将上面的函数进行一个修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Rectangle {
constructor (length, width) {
this.length = length;
this.width = width;
};

getArea () {
return this.width * this.length;
}
}

class Square extends Rectangle {
constructor (length, width) {
super(length, width)
}
}

let square = new Square(2, 4);
console.log(square.getArea()); // 8
console.log(square instanceof Square) // true
console.log(square instanceof Rectangle) // true

类的出现让我们轻松的实现了继承功能,使用extends可以指定类的继承函数,原型会自己调整,然后通过super()方法即可快速访问父类的构造函数。

super关键字

下面我们来详细看一下super关键字

super这个关键字既可以当做函数使用,也可以当做对象使用

第一种情况,super作为函数调用时代表父类的构造函数,ES6中有明确的规定,子类的构造函数必须执行一次super函数,但是如果我们不使用构造函数,当创建新的类实例时就会自动调用super()方法,并传入所有的参数。

1
2
3
4
5
6
7
8
9
10
11
class Square extends Rectangle {
// 没有构造函数
}

// 等价于

class Square extends Rectangle {
constructor (length, width) {
super(length, width)
}
}

super虽然代表了父类A的构造函数,但是返回的却是子类B的实例,也就是说super内部this指向的是B,因此super()在这里相当于A.prototype.constractor.call(this)

1
2
3
4
5
6
7
8
9
10
11
12
class A {
constructor () {
console.log(new.target.name);
}
}
class B extends A {
constructor () {
super();
}
}
new A(); // A
new B(); // B

上面的代码中,new.target指向的正是当前执行的函数。从结果中我们可以看到,在super()执行的时候它指向的是子类B的构造函数,而不是父类A的构造函数,也就是说,super()内部this的指向是B。
super在使用的时候还有另外一种情况–super作为对象时在普通方法中指向父类的原型对象,在静态方法中指向父类

1
2
3
4
5
6
7
8
9
10
11
12
class A {
p () {
return 1;
}
}
class B extends A{
constructor () {
super();
console.log(super.p()); // 1
}
}
new B();

上面的代码中,子类B中的super就是当成一个对象来使用的,此时的super就相当于普通函数中的A.prototype,所以super.p()就相当于A。prototype.p()。
还有一点需要注意的是,在子类的构造函数中,如果需要访问父类的属性可以使用this来实现,但是只有调用了super()方法之后才可以使用this关键字,否则会报错,这是因为子类实例的构建是基于对父类实例的加工,只有super方法才能够返回父类的实例。

下面我们来总结一下super()的使用需要注意的一些细节

  • 只可以在派生类的构造函数中使用super()方法,如果尝试在非派生类或函数中使用则会导致程序抛出异常。
  • 如果不想使用super(),则唯一的办法是让父类的构造函数返回一个对象。
  • 在派生类的构造函数中使用this之前一定要先调用super()方法,如果次序颠倒程序会抛出异常。
  • super指向的是父类的原型对象,所以定义在父类实例上的属性和方法是无法通过super方法调用的

下面我们再来看一个容易出错的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
constructor () {
this.x = 1;
}
print () {
console.log(this.x)
}
}
class B extends A {
constructor () {
super();
this.x = 2;
}
m () {
super.print();
}
}
let b = new B();
b.m(); // 2

上面的代码中,super.print()虽然调用的是父类A.prototype.print(),但是A.prototype.print却被添加到子类B的this中,所以代码输出结果是2,所以上述代码相当于执行A.prototype.print.call(this)。同理如果使用super赋值,赋值的属性会变成子类实例的属性,此时的super就是this。
下面我们再来看看super作用在静态方法中的情况,借用《ES6标准入门》中的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

上面的代码中,super在静态代码中指向父类,在普通方法中指向父类的原型对象。
另外在使用super的时候我们必须显式指定是作为对象还是作为函数使用,因为super无法看出是作为函数使用还是作为对象使用,所以在解析代码时就会报错。

extends关键字

说完了super关键字,我们再来说说extends关键字,extends可以继承任何类型的表达式,只要该表达式最终返回的是一个可继承的函数,也就是说extends可以继承具有prototype属性的函数,由于函数都有prototype属性(除了Function函数),因此A可以是任意函数。
那么 ES6 extends继承到底做了什么操作?
我们先来看看这段包含静态方法的ES6继承代码

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
// ES6
class Parent{
constructor(name){
this.name = name;
}
static sayHello(){
console.log('hello');
}
sayName(){
console.log('my name is ' + this.name);
return this.name;
}
}
class Child extends Parent{
constructor(name, age){
super(name);
this.age = age;
}
sayAge(){
console.log('my age is ' + this.age);
return this.age;
}
}
let parent = new Parent('Parent');
let child = new Child('Child', 18);
console.log('parent: ', parent); // parent: Parent {name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log('child: ', child); // child: Child {name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18

这其中包含了两条原型链,我们来看看具体代码

1
2
3
4
5
6
7
8
9
10
// 1、构造器原型链
Child.__proto__ === Parent; // true
Parent.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 2、实例原型链
child.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true

结合以上代码我们可以看出:ES6的extends主要就是

  1. 把子类构造函数(Child)的原型(proto)指向了父类构造函数(Parent)
  2. 把子类实例child的原型对象(Child.prototype) 的原型(proto)指向了父类parent的原型对象(Parent.prototype)。
  3. 子类构造函数Child继承了父类构造函数Preant的里的属性。使用super调用的(ES5则用call或者apply调用传参)。

本文完