类的继承可以通过extends实现,让子类继承父类的属性和方法,而在子类内部(构造函数constructor)必须调用super()实现继承(super()代表父类构造函数,调用之后生成一个继承父类的this对象)
super()作为函数调用时(子内constructor内部必须调用),代表父类的构造函数,调用之后生成的是子类的实例,即super()内部this指向的是子类的实例(由构造函数this指向可得出,构造函数内部this指向的是生成的实例,而super()调用之后生成的是子类实例)
内部的this指向函数运行时所在的对象,也即指向函数的直接调用者
没有自己的this对象,内部的this就是定义时上层作用域中的this
构造函数只能是普通函数,箭头函数不能作为构造函数(没有prototype),不可以对箭头函数使用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指向实例对象
下面代码中,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执行的构造函数
super作为对象时,在子类普通方法中,指向父类的原型对象;在子类静态方法中,指向父类
当super作为对象在子类普通方法(非静态方法)中使用时,由于super指向父类的原型对象,所以定义在父类原型对象上的属性和方法都可以被访问到,而定义在父类实例上(父类构造函数this.xxx)的方法或属性无法被访问到
在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例,相当于执行的是super.fn.call(this)
下面的代码中,父类A有定义在原型对象的属性b和方法p(),还有定义在实例对象的属性a;可以看到的是,定义在原型对象的b和p都能被super.访问到,而定义在实例对象的a无法被访问到
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()
在子类普通方法中通过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调用父类的方法时,方法内部的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
通过本文的介绍,我们了解了在ES6中使用extends关键字实现继承,并且学习了super()在子类中的用法和作用以及注意点。super()的正确使用可以让我们更加方便地在子类中调用父类的构造函数和方法,从而更加灵活地使用面向对象编程。希望本文能够帮助各个小伙伴更好地理解ES6中Class继承的super()关键字。