在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()) console.log(square instanceof Square) console.log(square instanceof Rectangle)
|
以上代码简单的实现了一个继承过程。为了实现这个继承过程,我们先创建了子类对象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()); console.log(square instanceof Square) console.log(square instanceof Rectangle)
|
类的出现让我们轻松的实现了继承功能,使用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(); new 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()); } } 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();
|
上面的代码中,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); let child = new Child(); child.myMethod(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
| 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.sayHello(); parent.sayName(); console.log('child: ', child); Child.sayHello(); child.sayName(); child.sayAge();
|
这其中包含了两条原型链,我们来看看具体代码
1 2 3 4 5 6 7 8 9 10
| // 1、构造器原型链 Child.__proto__ === Parent Parent.__proto__ === Function.prototype Function.prototype.__proto__ === Object.prototype Object.prototype.__proto__ === null // 2、实例原型链 child.__proto__ === Child.prototype Child.prototype.__proto__ === Parent.prototype Parent.prototype.__proto__ === Object.prototype Object.prototype.__proto__ === null
|
结合以上代码我们可以看出:ES6的extends主要就是
- 把子类构造函数(Child)的原型(proto)指向了父类构造函数(Parent)
- 把子类实例child的原型对象(Child.prototype) 的原型(proto)指向了父类parent的原型对象(Parent.prototype)。
- 子类构造函数Child继承了父类构造函数Preant的里的属性。使用super调用的(ES5则用call或者apply调用传参)。
本文完