ES6 Class 继承
简单谈谈
ES6
规定了 extends
关键字,用该关键字就可以实现继承,对比 ES5
通过修改原型链来实现继承,可以说简洁了不少。
class Point {
};
class ColorPoint extends Point {
constructor(x, y, color) {
// 调用父类的constructor(x, y)
super(x, y);
this.color = color;
}
toString() {
// 调用父类的toString()
return this.color + ' ' + super.toString();
}
}
当然子类如果没有定义 constructor
方法,这个方法会被默认添加。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
子类必须在 constructor
方法中调用 super
方法,否则新建实例时会报错。这是因为子类自己的 this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super
方法,子类就得不到 this
对象。
如上所述,如果不调用 super
方法,子类就得不到 this
对象。那么如果在调用 super
方法前使用 this
,那么就会报错。
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 A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello(); // hello world
getPrototypeOf
用于从子类上获取父类
Object.getPrototypeOf(ColorPoint) === Point;
// true
super 关键字
super
关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
函数调用
当 super
作为函数调用时,代表父类的构造函数。
而且作为子类必须调用 super
函数,但是需要注意的是虽然是父类的构造函数,但返回的是子类的实例。
class A {}
class B extends A {
constructor() {
super();
}
}
super
虽然代表了父类 A
的构造函数,但是返回的是子类 B
的实例,即 super
内部的 this
指的是 B
,因此 super()
在这里相当于 A.prototype.constructor.call(this)
。
作为函数时, super()
只能用在子类的构造函数之中,用在其他地方就会报错。
作为对象使用
super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
在方法中使用
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p());
// 2
}
}
let b = new B();
在静态方法中使用
class A {
static p1() {
return 2;
}
}
class B extends A {
static p2() {
super.p1();
// 2
}
}
super
当成对象使用时,如果在子类方法中使用,super
的指向为父类的 prototype
,如果在子类的静态方法中使用则指向父类。
进一步理解:子类方法其实也是在子类的 prototype
下,所以对应关系为:在子类的 prototype
中使用 super
那么就是指向父类的 prototype
,在子类下(也就是静态方法)使用 super
那么就是指向父类。
所以 super
的表现和 this
是差不多一致的,唯一不同的是 super
是针对父类的引用。
还需要注意一点的是:如果通过 super
调用父类的方法时,方法内部的 this
是指向子类的(不论是不是在静态方法中使用)。
class Parent {
static staticMethod() {
console.log(this.staticType);
}
constructor() {
this.type = 'parent';
}
method() {
console.log(this.type);
}
}
Parent.staticType = 'parent static';
class Child extends Parent {
static staticMethod2() {
super.staticMethod();
}
constructor() {
super();
this.type = 'child';
}
method2() {
super.method();
}
}
Child.staticType = 'child static';
Child.staticMethod2(); // child static
var child = new Child();
child.method2(); // child
proto
ES5
中每一个对象都拥有 __proto__
属性,指向其构造函数的 prototype
。通过 __proto__
可以实现对象的继承。
ES6
中,出现了类层面上的继承,因此规定了另外一条继承链,构造函数的继承。
class A {
}
class B extends A {
}
// ES6 中新出现的构造函数的继承
B.__proto__ === A // true
// 与 ES5 一致,原型链的继承
B.prototype.__proto__ === A.prototype // true
因此在 ES6
中,__proto__
即可以用来表示原型链的继承关系,也可以用来表示构造函数的继承关系。
当然出现这条继承链的原因是因为 ES6
中继承是按照下面的模式实现的。
class A {
}
class B {
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
const b = new B();
而 setPrototypeOf
是这样实现的。
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
所以当 B
继承 A
的静态属性时,就会顺便在 B
类下设置一个 __proto__
。
当然如果一个类没有继承任何的类那么 __proto__
会指向哪?
class A {
}
A.__proto__ === Function.prototype; // true
A.prototype.__proto__ === Object.prototype; // true
这种情况下, A
作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承 Function.prototype
。但是, A
调用后返回一个空对象(即 Object
实例),所以 A.prototype.__proto__
指向构造函数( Object
)的 prototype
属性。
原生构造函数的继承
目前常用的 ECMAScript
原生构造函数大致如下
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
ES5
中,这些构造函数是无法被继承的。
function MyArray() {
Array.apply(this, arguments)
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
})
上述代码生成了一个继承的 MyArray
类,但这个类的行为与 Array
完全不一致
var colors = new MyArray();
colors[0] = "red";
colors.length; // 0
colors.length = 0;
colors[0]; // "red"
之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过 Array.apply()
或者分配给原型对象都不行。原生构造函数会忽略 apply
方法传入的 this
,也就是说,原生构造函数的 this
无法绑定,导致拿不到内部属性。
照成这样的根本原因是:ES5
创建实例时,先由子类生成 this
对象,在用这个对象去添加父类下的方法,而父类的内部属性仅仅只能在父类生成的 this
对象下使用,这样就会导致子类生成的 this
对象访问不到父类的内部属性,导致子类的行为异常。
而在 ES6
中,构造函数生成实例时,是先由父类生成 this
对象,然后用子类的构造函数去修改/添加 this
对象下的方法或属性,这样就使得 this
对象可以获得父类的所有行为。以下例子可以说明
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length; // 1
arr.length = 0;
arr[0]; // undefined
上述代码就完成了对原生对象的继承。
可以定义一个 Error
的子类,指定相应的报错信息
class ExtendableError extends Error {
constructor(message) {
super();
this.message = message;
this.stack = (new Error()).stack;
this.name = this.constructor.name;
}
}
class MyError extends ExtendableError {
constructor(m) {
super(m);
}
}
var myerror = new MyError('ll')
myerror.message; // "ll"
myerror instanceof Error; // true
myerror.name; // "MyError"
myerror.stack;
// Error
// at MyError.ExtendableError
// ...
继承 Object
构造函数时需要注意。
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
上面代码中,NewObj
继承了 Object
,但是无法通过 super
方法向父类 Object
传参。这是因为 ES6
改变了 Object
构造函数的行为,一旦发现 Object
方法不是通过 new Object()
这种形式调用,ES6
规定 Object
构造函数会忽略参数。