前面的话
es5之前的继承大多以原型链继承为主,但原型链继承的缺点很大。es6引入的类的继承方便的了不少。
es5模拟继承
es5 实现继承需要多个步骤:
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function () {
return this.length * this.width;
}
function Square(length) {
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value: Square,
enumerable: true,
writable: true,
configurable: true
}
});
var square = new Square(3);
console.log(square.getArea());// 9
console.log(square instanceof Square);// true
console.log(square instanceof Rectangle);// true
Square 继承了Rectangle,为了这样做,必须用一个Object.create()创建一个原型为Rectangle.prototype 的对象来重写Square.prototype,并且要调用Rectangle.call()方法,改变this的指向。
super关键字
es6中的类让我们很轻松的实现继承的功能,使用熟悉的extends关键字可以指定类继承的函数。原型会自动调整,通过调用super()方法即可访问基类的构造函数。
super关键字既可以当作函数使用,也可以当作对象使用。这两种情况下,它的用法完全不同。
[作为函数调用]
作为函数调用时代表父类的构造函数。
class Rectangle {
constructor (length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor (length) {
// 与Rectangle.call(this, length, length)相同
super(length, length);
}
}
var square = new Square(3);
console.log(square instanceof Square);// true
console.log(square instanceof Rectangle);// true
Square类通过extends关键字继承Rectangle类,在Square构造函数中通过super()调用Rectangle构造函数,并传入相应的参数。
这里子类Square的构造函数中的super()代表调用父类的构造函数,但super内部的this指向的是Square。 因此super()在这里相当于 Square.prototype.constructor.call(this).
[注意]
-
继承自其他类的派生类(子类)在constructor方法中必须调用super()方法,否则新建实例会报错。
-
在子类的构造函数中,只有调用了super之后才可以使用this。
class Rectangle { constructor (length, width) { this.length = length; this.width = width; } getArea () { return this.length * this.width; } } class Square extends Rectangle { constructor(length){ // this.length = length; // 报错 super(length,length); this.length = length; } } let square = new Square(5); console.log(square.length);// 5 console.log(square.getArea());// 25
原因是因为子类没有自己的this对象,而是继承的父类的this对象,然后对其加工。如果不调用super()方法, 子类就得不到this对象。
-
如果子类没有定义constructor方法,那么这个方法会被默认添加
class Square extends Rectangle { // 没有构造器 } // 等价于 class Square extends Rectangle { constructor(...args) { super(...args) } }
[作为对象]
- 在普通方法中指向父类的原型对象
- 在静态方法中指向父类
在普通方法中:
class A {
constructor() {
this.x =1;
}
p() {
return 2;
}
print() {
console.log(this.x);
}
}
A.prototype.y = 3;
class B extends A {
constructor() {
super();
this.x = 4;
console.log(super.p()) ;
}
get m() {
return super.x;
}
get n() {
return super.y;
}
G() {
super.print();
}
}
let b = new B(); // 2
console.log(b.m);// undefined
console.log(b.n);// 3
b.G();// 4
在创建b实例时,constructor方法内执行super.p(),这里的super为对象,指向A.prototype,相当于A.prototype.p(),所以会输出2。
在调用b.m时,执行super.x,相当于A.prototype.x,而A.prototype没有x,只有实例的x,所有定义在父类实例上的方法与属性是无法通过super调用的。
在调用b.n时,此时A.prototype.y = 3,所以输出3。
在调用b.G()时,返回的是B类实例上的x,说明super调用父类方法时,还是会绑定子类的this。所以输出4。
在静态方法中:
super将指向父类本身,而不是父类原型对象
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在静态方法中指向父类。
在执行Child.myMethod(1)时,执行的是子类的静态方法,在静态方法中,super指向的是父类super.myMethod(1),相当于parent.myMethod(1),即执行父类的静态方法,所以输出 ‘static 1’。
在执行child.myMethod(2)时,访问的是子类原型上的方法myMethod,这个方法是一个普通函数,super指向父类的原型,相当于执行父类原型上的myMethod方法,所以输出‘instance 2’。
类的Prototype属性与__proto__属性
Class作为构造函数的语法糖,同时拥有prototype属性和__proto__属性,因此同时存在两条继承链
- 子类的__proto__属性表示构造函数的继承,总是执行父类
- 子类的prototype属性的__proto__属性表示方法的继承,总是指向父类的prototype属性
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent{
constructor(name, age) {
super(name);
this.age = age;
}
getName() {
console.log(this.name);
}
}
let parent = new Parent('xiao')
let child = new Child('xiaoqi', 20);
console.log(Child.prototype.__proto__ === Parent.prototype);// true
console.log(Child.__proto__ === Parent );// true
console.log(child.__proto__ === Child.prototype);// true
console.log(child.__proto__.__proto__ === parent.__proto__ );// true
这两条继承链可以这样理解:
-
作为对象,子类的原型(__proto__属性)是父类
-
作为构造函数,子类的原型(prototype属性)是父类的实例
Object.create(Parent.prototype); // 等同于 Child.prototype.__proto__ = Parent.prototype;
另外,子类实例的__proto__的__proto__属性指向父类实例的__proto__属性,即子类的原型的原型是父类的原型
原生构造函数的继承
使用es5定义一个继承Array的函数:
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
})
var colors = new MyArray();
colors[0] = 'red';
console.log(colors.length);// 0
colors.length = 0;
console.log(colors[0]);// 'red'
之所以发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。
在es5中 ,先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。
es6允许继承原生构造函数,因为es6先新建父类的实例对象this,然后在用子类的构造函数修饰this,使得父类的所有行为都得到继承。
class MyArray extends Array{
constructor(...args){
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
console.log(arr.length);// 1
arr.length = 0;
console.log(arr[0]);// undefined
}
获取父类
Object.getPrototypeOf():
使用该方法可以用来从子类上获取父类,即可以判断一个类是否继承了另一个类。
class Parent {
}
class Child extends Parent {
}
console.log(Object.getPrototypeOf(Child) === Parent);// true