ES6中引入了class
关键字来作为实现类
的语法糖,与此同时,ES6中的类也支持继承
的概念,以下为继承的学习总结
一、简介
一个简单的继承例子为:
class A {
// some codes
}
class B extends A {
// some codes
}
如此一来,便实现了B –继承–> A,但是有一些注意事项:
1)B的构造函数中,必须要有super()
来调用父类的构造函数。如果没有,则会报错。这是因为:ES6中的继承,和ES5中是不一样的。在ES5中,实例是先创造子类的实例对象,再用这个实例对象去作为父类构造函数
的this(即在B中有:A.apply(this)
),但是ES6中,继承的实质是:
先创造父类的实例对象,然后再用子类的构造函数来修改this。所以,如果没有调用super()
,那么子类就无法得到this,this就未定义了,所以如果没有调用super()
,那么子类中无法使用this
关键字
2)如果子类中没有显式指明constructor
,那么子类中默认会添加:
constructor (...args) {
super(...args);
}
生成的实例中,会有关系如:
const b = new B;
b instanceof B; // true
b instanceof A; // true
二、判断继承关系
可以使用Object.getPrototypeOf()
方法来判断一个类是否继承了另一个类,有:
Object.getPrototypeOf(B) === A;
三、详解super关键字
在ES6的class继承中,有一个很重要的关键字:super
,关于super
,它有以下的作用:
1)作为函数调用时,super
表示的是父类的构造函数
2)作为对象调用时,super
表示的是父类的原型对象(即Parent.prototype
)
3)作为对象且进行赋值操作时,super
相当于是this
4)在static方法中调用时,super
表示的是父类本身
具体解释如:
1、super作为构造函数
ES6中要求:子类中必须执行一次构造函数,所以有:
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
我们可以看到,输出的结果是:B
。所以我们可以得到如下结论:
super
虽然代表的是父类的构造函数,但其内部的this指向的是子类的实例。所以super()
相当于A.prototype.constructor.call(this)
还需要注意的是:super()
仅能用于构造函数中,用在其他地方就会报错
2、super作为对象时
先得到一个结论:
1)作为getter使用时,super相当于父类的原型对象
2)作为setter使用时,super相当于子类中的this
当super
作为对象时,在普通方法中,它指向的是父类的原型对象;在静态方法中,指向的是父类,如:
class A {}
A.prototype.hello = function () {
console.log('Hello!');
}
class B extends A {
constructor() {
super();
super.hello();
}
}
new B; // 输出:Hello!
因为super
指向的是父类原型对象,所以在父类实例对象上定义的方法、属性是无法使用super
调用的,如:
class A {
constructor() {
this.str = 'Hello!';
}
}
class B extends A {
constructor() {
super();
console.log(super.str);
}
}
const b = new B; // 输出:undefined
b.str; // Hello!
虽然不能用super
调用,但是可以用this
调用!
此前说过,当使用super
调用父类方法时,super
内部的this,绑定是子类实例对象,所以我们可以实验得知:
class A {
describe() {
console.log(this.str);
}
}
class B extends A {
constructor() {
super();
this.str = 'I am in B!';
}
}
(new B).describe(); //输出:I am in B!
使用super
对某个属性赋值时,super
相当于子类的this
,而非父类:
class A{}
class B extends A {
constructor() {
super();
this.x = 1;
super.x = 2;
console.log(super.x);
console.log(this.x);
}
}
new B;
/*
输出:
undefined
2
*/
这是因为,使用super.x = 2
时,相当于执行了this.x = 2
,而获取super.x
时,相当于获取A.prototype.x
(undefined)
3、当在static
方法中使用super
的时候,super
代表的是父类,如:
class A {
static foo() {
console.log('I am static!');
}
foo() {
console.log('I am common!');
}
}
class B extends A {
static myFoo() {
super.foo();
}
myFoo() {
super.foo();
}
}
B.myFoo(); // 输出:I am static!
(new B).myFoo(); // 输出:I am common!
4、使用super时必须明确指定
使用super
时,必须明确指定是作为函数
使用,还是作为对象
使用,即以下用法是错误的:
console.log(super);
正确做法:要么以super()
形式调用,要么以super.xxx
形式调用
四、类的prototype属性和proto属性
大多数浏览器的ES5实现中,每个对象都有__proto__
属性,指向其构造函数的原型对象。而Class实际上是构造函数的语法糖,它也同时拥有原型对象属性(prototype
)和__proto__
属性:
1)prototype
表明它作为构造函数,理应有一个原型对象属性。所以有:子类.prototype.__proto__ === Parent.prototype
2)__proto__
表示构造函数的继承,所以SubClass.__proto__ === Parent
即类的继承是以如下方式建立的:
/*
示例代码:
class A {}
class B extends A {}
*/
Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(B, A);
五、extends的继承目标
extends
后面可以跟多种类型的值,甚至表达式也是可以的,只要其或其返回值是一个有prototype
属性的函数,就可以被继承。此外,需要注意三种特殊情况:
1)子类继承Object
class A extends Object {
}
A.__proto__ === Object;
A.prototype.__proto__ === Object.prototype;
这种情况下,A就是构造函数Object
的复制,A的实例就是Object
的实例
2)没有继承
class A {}
A.__proto__ === Function.prototype;
A.prototype.__proto__ === Object.prototype;
3)继承null
class A extends null {
}
A.__proto__ === Function.prototype;
A.prototype.__proto__ === undefined;
六、原生构造函数的继承
原生构造函数是语言内置的构造函数,ECMAScript中的原生构造函数大致有:
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
在ES6之前,这些原生的构造函数是无法继承的,如:
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: MyArray,
writable: true
}
});
const ma = new MyArray();
ma[0] = 'HelloWorld';
const a = new Array();
a[0] = 'HelloWorld';
ma.length; // 0
a.length; // 1
可以发现,两者行为不一致。这是因为:子类无法完全获得原生构造函数的内部属性,即使用Array.apply()
也是不行的,原生构造函数会忽略apply
方法传入的this
,所以没办法绑定this,从而也就拿不到内部属性。
由于ES6是先新建父类实例对象this
,然后再用子类的构造函数修饰this
,所以就使得父类的所有行为都能够继承:
class MyArray extends Array{
}
const ma = new MyArray();
ma[0] = 'HelloWorld';
ma.length; // 1
不过,使用ES6继承Object
的话,会有一个行为差异:在ES6中,一旦发现Object方法不是通过new Object()
这种形式调用,那么Object
构造函数会忽略参数:
class NObj extends Object{
}
const obj = new NObj({attr: true});
obj; // {}
七、Mixin的实现
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, min.prototype);
}
return Mix;
}
function copyProperties(target, source) {
const keys = Reflect.ownKeys(source).filter(key => {
return ['constructor', 'prototype', 'name'].includes(key);
});
keys.forEach(key => {
Object.defineProperty(
target,
key,
Object.getOwnPropertyDescriptor(source, key)
)
});
}