ES6的Class继承--super关键字
theme: cyanosis
概述
类的继承可以通过extends
实现,让子类继承父类的属性和方法,而在子类内部(构造函数constructor)必须调用super()
实现继承(super()
代表父类构造函数,调用之后生成一个继承父类的this
对象)
继承机制
ES6 的继承机制,与 ES5 完全不同:
- ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”
- ES6 的继承机制,则是先将父类的属性和方法,加到一个空对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”
- 所以 ES6 的继承必须先调用
super()
,这样会生成一个继承父类的this
对象,没有这一步就无法继承父类。这意味着新建子类实例时,父类的构造函数必定会先运行一次 super
既可以作为函数使用,也可以作为对象使用,两种方式有很大的不同,详见下文
super
作为函数
super()
作为函数调用时(子内constructor
内部必须调用),代表父类的构造函数,调用之后生成的是子类的实例,即super()
内部this
指向的是子类的实例(由构造函数this
指向可得出,构造函数内部this
指向的是生成的实例,而super()
调用之后生成的是子类实例)
构造函数 this
【首先要明确this
指向】:
- 普通函数:内部的
this
指向函数运行时所在的对象,也即指向函数的直接调用者- 如果一个函数在全局环境运行,this就指向顶层对象(浏览器中为window对象)
- 如果一个函数作为某个对象的方法运行,this就指向那个对象;
- 如果一个函数作为构造函数,this指向它的实例对象
- 箭头函数:没有自己的
this
对象,内部的this
就是定义时上层作用域中的this
要注意的是,构造函数只能是普通函数,箭头函数不能作为构造函数(没有prototype
),不可以对箭头函数使用new
命令,否则会抛出一个错误
【其次要知道new
操作符做了哪些事情】:
- 创建一个空对象
instance
({}
) - 将
instance
的[[prototype]]
属性指向构造函数fn
的原型(即instance.[[prototype]] = fn.prototype
),也即instance
的__proto__
要指向构造函数的原型prototype
- 执行构造函数,使用
call/apply
改变this
的指向 => 让this
指向创建出来的实例instance
- 若函数返回值的是对象类型则作为
new
方法的返回值返回,否则返回刚才创建的全新对象
function isObject (target) {
return (typeof target === 'object' || typeof target === 'function') && target !== null
}
function myNew (fn, ...args) {
// 使用Object.create(Y),就说明实例的__proto__指向了Y,即实例的原型对象是Y
// Object.create():创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
// 并且执行[[Prototype]]链接; 通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
const instance = Object.create(fn.prototype)
// 改变this指向
let res = fn.apply(instance, args)
// 若res是个对象就直接返回,否则返回创建的对象
// return res instanceof Object ? res : instance
return this.isObject(res) ? res : instance
}
如果不用new
操作符直接调用,那么构造函数就相当于普通函数了,执行对象就 是window
(严格模式下为undefined
),即this
指向了window
(严格模式下为undefined
)
由此可以看出,构造函数内部this
指向实例对象
super()
的用法和注意点
【用法】
下面代码中,B
是 A
的子类,B
继承 A
,super
虽然代表了父类A
的构造函数,但是返回的是子类B
的实例,即super
内部的this
指的是B
的实例
B
里面的super()
在这里相当于A.prototype.constructor.call(this)
class A {
constructor() {
// `new.target`指向当前正在执行的函数
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
// 在`super()`执行时,它指向的是子类`B`的构造函数,而不是父类`A`的构造函数
// 即`super()`内部的`this`指向的是`B`的实例
new B() // B
new.target
指向被new
调用的构造函数:在普通的函数调用中(函数调用),new.target
的值是undefined
;在类的构造方法中,new.target
指向直接被new
执行的构造函数
【注意点】
子类的构造函数
constructor
内必须执行一次super
函数- 这是因为子类自己的
this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()
方法,子类就得不到自己的this
对象 - 新建子类实例时,父类的构造函数必定会先运行一次
- 这是因为子类自己的
在子类的构造函数中,只有调用
super()
之后,才可以使用this
关键字,否则会报错super()
调用之后才能让子类实例继承父类,然后才能在该基础上对子类实例进行属于它的特有操作
不管有没有显式定义,任何一个子类都有
constructor()
方法,而且里面默认会调用super()
作为构造函数时,
super()
只能用在子类的构造函数之中,用在其他地方就会报错class A {} class B extends A { m() { super(); // 报错 } }
使用
super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错class A {} class B extends A { constructor() { super(); console.log(super); // 报错 } }
由于对象总是继承其他对象的,所以可以在任意一个对象中,使用
super
关键字var obj = { toString() { return "MyObject: " + super.toString(); } }; obj.toString(); // MyObject: [object Object]
super
作为对象
super
作为对象时,在子类普通方法中,指向父类的原型对象;在子类静态方法中,指向父类
super
在子类普通方法中作为对象
当super
作为对象在子类普通方法(非静态方法)中使用时,由于super
指向父类的原型对象,所以定义在父类原型对象上的属性和方法都可以被访问到,而定义在父类实例上(父类构造函数内this.xxx
)的方法或属性无法被访问到
- 在子类普通方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类实例,相当于执行的是super.fn.call(this)
下例,父类A
有定义在原型对象的属性b
和方法p()
,还有定义在实例对象的属性a
;可以看到的是,定义在原型对象的b
和p
都能被super.
访问到,而定义在实例对象的a
无法被访问到
/* ES6的class其实可以看成语法糖,新的`class`写法只是让对象原型的写法更加清晰、更像面向对象编程的语法
构造函数写法和下面的类写法是等价的
function A () {
this.a = 123
}
A.prototype.p = function () {
return 2
}*/
class A {
constructor() {
this.a = 123 // 定义在A的实例上的属性a
}
// 定义在A.prototype上的方法
p() {
return 2;
}
}
A.prototype.b = 123 // 定义在A原型对象的属性b
class B extends A {
constructor() {
super();
console.log(super.p()); // 2 => 相当于console.log(A.prototype.p())
}
getData() {
console.log(super.a) // a定义在父类的实例对象上,所以为undefined
console.log(super.b) // b定义在父类的原型对象上,所以为123
}
}
let b = new B();
b.getData()
// undefined
// 123
在子类普通方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类实例,相当于执行的是 super.fn.call(this)
class A {
constructor() {
this.x = 1; // 父类实例的x是1
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2; // 子类实例的x是2
}
m() {
// 实际上执行的是`super.print.call(this)`
super.print(); // 调用父类的方法,但是方法内部this是子类实例,所以输出2
}
}
let b = new B();
b.m() // 2
由于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
作为对象在子类静态方法中使用时,这时super
将指向父类,而不是父类的原型对象
- 在子类的静态方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类,而不是子类的实例
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
// super作为对象在子类静态方法中使用,super指向的是父类
// 也即要调用父类的myMethod方法,那就是父类的静态方法Parent.myMethod
// 而属于一个类的方法,就是静态方法,也就是由static关键字定义的方法
super.myMethod(msg); // 调用的是Parent.myMethod()
}
myMethod(msg) {
// super作为对象在子类普通方法中使用,super指向的是父类的原型对象
// 此时的super.myMethod()也即Parent.prototype.myMethod()
// 由class语法糖和原来构造函数创建类的写法相对比可知
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
总结
子类通过extends
实现继承父类,子类constructor
里必须调用super()
来实现继承(super()
在这里表示父类的构造函数):
- 因为ES6继承机制就是调用
super()
生成一个继承父类的this
然后把它作为子类的实例 - 子类构造函数内只有在调用了
super()
之后才能使用this
- 要注意的是,
super()
作为父类构造函数只能在子类constructor
中使用 - 且
super()
内部的this
指向的是子类的实例,即可以把super()
看成是父类.prototype.constructor.call(this)
当super
作为对象在子类普通方法(非静态方法)中使用时:
super
指向的是父类的原型对象,所以定义在父类原型对象的属性和方法可以被super
访问到,而定义在父类实例的属性和方法就不能被super
访问到super.fn()
内部的this
指向的是子类的实例,相当于执行super.fn.call(this)
;- 如果通过
super
对某个属性赋值,由于this
指向子类实例,这时super
就是this
,赋值的属性会变成子类实例的属性
当super
作为对象在子类静态方法中使用时:
super
指向的是父类,所以定义在父类的属性和方法可以被super
访问到,由类的定义可知,定义在类上的属性和方法就是类的静态属性和静态方法,也就是类中用static
定义的方法,可以直接用类去调用而不是通过实例调用- 而在子类静态方法中调用父类方法
super.fn()
,也即调用父类.fn()
(这是一个父类的静态方法static fn()
),内部this
指向的是子类
对于上面在子类普通方法或静态方法中调用
super.xxx()
,super.xxx()
内部this
指向,个人的理解是:super.xxx()
更像是对父类方法的一个引用,而实际的this
还要看当前所在子类方法的this
,而this
指向可以总结为谁直接调用该方法,它内部的this
就指向那个对象 -- 子类普通方法是由子类实例调用的,所以方法内部this
指向子类实例,那么super.xxx()
内部的this
也就指向子类实例;而子类静态方法是由子类直接调用的(即子类.xxx()
),所以方法内部this
指向子类,那么super.xxx()
内部的this
也就指向子类了