1、简介
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方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
class Point { /*......*/ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint()
上面代码中,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方法才能调用父类实例。
2、Object.getPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint) === Point
// true
3、super关键字
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这种情况下,它的用法完全不同。
第一种情况,super作为函数调用时,代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数。
class A { }
class B extends A {
constructor() {
super()
}
}
注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)。
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()只能用在子类的构造函数之中,用在其他地方就会报错。
class A { }
class B extends A {
m() {
super() // 报错
}
}
第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;静态方法中,指向父类。
class A {
p () {
return 2
}
}
class B extends A {
constructor() {
super()
console.log(super.p()) // 2
}
}
let b = new B();
上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。
注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。
class A {
constructor() {
this.p = 2
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B()
b.m // undefined
上面代码中,p是父类A实例的属性,super.p就引用不到它。
如果属性定义在父类的原型对象上,super就可以取到。
class A {}
A.prototype.x = 2
class B extends A {
constructor() {
super()
console.log(super.x) // 2
}
}
let b = new B()
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)
}
myMethod(msg) {
super.myMethod(msg)
}
}
Child.myMethod(1) // static 1
var child = new Child()
child.myMethod(2) // instance 2
另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
注意,使用super的时候,必须显示指定是作为函数,还是作为对象使用,否则会报错。
class A { }
class B extends A {
constructor() {
super()
console.log(super) //报错
}
}
上面代码中,console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以JavaScript引擎解析代码的时候就会报错。这时,如果能清晰地表明super的数据类型,就不会报错。
4、类的prototype属性和proto属性
Class作为构造函数的语法糖,同时有prototype属性和proto属性,因此同时存在两条继承链。
(1)子类的proto属性,表示构造函数的继承,总是指向父类。
(2)子类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()
Object.setPrototypeOf(B.prototype, A.prototype)
// 等同于
B.prototype._proto_ = A.prototype
Object.setPrototypeOf(B, A)
// 等同于
B._protot_ = A;
这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(proto属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。
B.prototype = Object.create(A.prototype)
//等同于
B.prototype._proto_ = A.prototype