Class 可以通过
extends
关键字实现继承, 这边 ES5通过原型链实现继承, 要清晰和方便很多class Point { } class ColorPoint extends Point { }
上面代码定义了一个
ColorPoint
类,该类通过extends
关键字,继承了Point
类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point
类。下面,我们在ColorPoint
内部加上代码。class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() } }
子类必须在
constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。class Point { /* ... */ } class ColorPoint extends Point { constructor() { } } let cp = new ColorPoint(); // ReferenceError
上面代码中,
ColorPoint
继承了父类Point
,但是它的构造函数没有调用super
方法,导致新建实例时报错。
ES5 的继承,实质是先创造子类的实例对象
this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到
this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
如果子类没有定义
constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor
方法。class ColorPoint extends Point { } // 等同于 class ColorPoint extends Point { constructor(...args) { super(...args); } }
另一个需要注意的地方是,在子类的构造函数中,只有调用
super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super
方法才能调用父类实例。也就是说, super() 方法必须在 constructor 方法体内的最上面调用
class Point { constructor(x, y) { this.x = x; this.y = y; } } class ColorPoint extends Point { constructor(x, y, color) { this.color = color; // ReferenceError super(x, y); this.color = color; // 正确 } }
父类的静态方法, 也会被子类继承
父类的类上的普通方法的继承
看以下案例:
class Animal { eat = this.eat // 父类的类上的方法(也就是父类的原型上的方法)不会被继承, 除非添加到实例 this上 constructor(name, age, sex) { this.name = name this.age = age this.sex = sex } sayHello() { // 父类的类上的普通方法不能被继承 console.log('父类的类上的普通方法sayHello...'); } eat(){ console.log('父类类上的普通方法, 显示添加到实例原型this 上 eat...'); } static staticFn(){ console.log('父类的静态方法,可以被继承,但是只能被子类调用, 不能被子类实例调用 staticFn...'); } } class Dog extends Animal { constructor(name, age, sex, color) { super(name, age, sex) this.color = color } sayHi() { console.log('-------- Dog类', this) } } const dog = new Dog('d', 12, 'b', 'yellow') dog.sayHi() dog.eat() // 父类的方法添加到 this 实例上之后可以被继承 Dog.staticFn() // 父类的静态方法可以被继承, 但是只能子类的类调用 dog.sayHello() // 父类的类上普通方法能被继承, 但是不在子类 this实例上
Object.getPrototypeOf
方法可以用来从子类获取父类class Animal { constructor(name, age, sex) { this.name = name } sayHello() { // 父类的类上的普通方法不能被继承 console.log('父类的类上的普通方法sayHello...'); } } class Dog extends Animal { constructor(name, age, sex, color) { super(name, age, sex) this.color = color } } # console.log(Object.getPrototypeOf(Dog) === Animal); // true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
super
这个关键字, 即可以当做函数使用, 也可以当做对象使用. 在这两种情况下, 它的用法完全不同.
第一种情况: ==
super
作为函数调用时, 代表父类的构造函数==ES6要求, 子类的构造函数必须执行一次super
函数class A {} class B extends A { constructor() { super(); } }
上面代码中,子类
B
的构造函数之中的super()
,代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。
注意,super
虽然代表了父类A
的构造函数,但是返回的是子类B
的实例,即super
内部的this
指的是B
的实例,因此super()
在这里相当于``A.prototype.constructor.call(this)
。
super()
内部的this
指向的是B
。作为函数时,
super
只能用在子类的构造函数中, 用在其他地方会报错.class A {} class B extends A { m() { super(); // 报错 } }
上面代码中,
super()
用在B
类的m
方法之中,就会造成语法错误。
第二种情况:
super
作为对象时.
- 在普通方法中, 指向父类的原型对象.
- 在静态方法中, 指向父类.
class Animal { constructor() { } sayHiParent(){ console.log('我是父类的普通方法... ...'); } } class Dog extends Animal { constructor() { super() } sayHi(){ super.sayHiParent() // 我是父类的普通方法 Animal.prototype.sayHiParent() // // 我是父类的普通方法 } } const dog = new Dog() dog.sayHi()
上面代码中, 子类
Dog
当中的super.sayHiParent()
, 就是将super
当做一个对象使用, 此时,super
在普通方法之中, 指向A.prototype,
, 所以super.sayHiParent()
就相当于Animal.prototype.sayHiParent()
这里需要注意: 由于
super
指向父类的原型对象, 所以定义在父类实例上的方法或属性, 是无法通过super
调用的.class Animal { constructor() { this.name = 'super实例上的属性' } } class Dog extends Animal { sayHi(){ return super.name } } const dog = new Dog() console.log(dog.sayHi()); // undefined 所以定义在父类实例上的属性, 无法通过super调用
在上面代码中
name
是父类Animal
实例的属性,super.name
就引用不到它.
如果属性定义在父类的原型对象上,
super
就可以取到class Animal { } Animal.prototype.name = 'super实例上的属性' class Dog extends Animal { sayHi() { return super.name } } const dog = new Dog() console.log(dog.sayHi()); // super实例上的属性
上面代码中, 属性
name
是定义在Animal.prototype
上面上的, 所以super.name
可以取到它的值.
ES6 规定, 在子类普通方法中, 通过
super
调用父类的方法时, 方法内部的this
指向当前子类的实例.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()
内部的this
指向子类B
的实例,导致输出的是2
,而不是1
。也就是说,实际上执行的是super.print.call(this)
。
由于
this
指向子类实例,所以如果通过super
对某个属性赋值,这时super
就是this
,赋值的属性会变成子类实例的属性。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.x
赋值为3
,这时等同于对this.x
赋值为3
。而当读取super.x
的时候,读的是A.prototype.x
,所以返回undefined
。
如果
super
作为对象, 用在静态方法之中, 这时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); // super在静态方法中指向父类, 而不是父类的原型 } myMethod(msg) { super.myMethod(msg); // super在普通方法中指向父类的原型 } } Child.myMethod(1); // static 1 var child = new Child(); child.myMethod(2); // instance 2
上面代码中,
super
在静态方法之中指向父类,在普通方法之中指向父类的原型对象。
另外,**在子类的静态方法中通过
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
上面代码中,静态方法
B.m
里面,super.print
指向父类的静态方法。这个方法里面的this
指向的是B
,而不是B
的实例
注意:
使用
super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。class A {} class B extends A { constructor() { super(); console.log(super); // 报错 } }
上面代码中,
console.log(super)
当中的super
,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明super
的数据类型,就不会报错。class A {} class B extends A { constructor() { super(); console.log(super.valueOf() instanceof B); // true } } let b = new B();
上面代码中,
super.valueOf()
表明super
是一个对象,因此就不会报错。同时,由于super
使得this
指向B
的实例,所以super.valueOf()
返回的是一个B
的实例。
大多数浏览器的 ES5 实现之中, 每一个对象都有
__proto__属性, 指向对一个的构造函数的
prototype属性, Class 作为构造函数的语法糖, 同时有
prototype属性和
proto 属性, 因此同时存在两条继承链
- 子类的
__proto__
属性, 表示构造函数的继承, 总是指向父类.- 子类的
prototype
属性的__proto__
属性, 表示方法的继承, 总是指向父类的prototype
属性.class A { } class B extends A { } B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true
上面代码中,子类
B
的__proto__
属性指向父类A
,子类B
的prototype
属性的__proto__
属性指向父类A
的prototype
属性。这样的结果是因为,类的继承是按照下面的模式实现的。
class A { } class B { } // B 的实例继承 A 的实例 Object.setPrototypeOf(B.prototype, A.prototype); // B 继承 A 的静态属性 Object.setPrototypeOf(B, A); const b = new B();
这两条继承链,可以这样理解:
- 作为一个对象,子类(
B
)的原型(__proto__
属性)是父类(A
);- 作为一个构造函数,子类(
B
)的原型对象(prototype
属性)是父类的原型对象(prototype
属性)的实例。
__proto__
属性子类实例的
__proto__
属性的__proto__
属性, 指向父类实例的__proto__
属性, 也就是说, 子类的原型的原型, 就是父类的原型.var p1 = new Point(2, 3); var p2 = new ColorPoint(2, 3, 'red'); p2.__proto__ === p1.__proto__ // false p2.__proto__.__proto__ === p1.__proto__ // true
上一章: ES6类Class 的基本语法, 静态方法, 实例属性新写法
下一章:
该博客为学习阮一峰 ES6入门课所做的笔记记录, 仅用来留作笔记记录和学习理解