文章编写参考 阮一峰《ECMAScript 6 入门》
1. 简介
Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
//基类
class People {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log("Hello,I am", this.name);
}
}
//子类
class Person extends People {
constructor(name, age, gender) {
super(name, age);
this.gender = gender;
}
sayHi() {
super.sayHi();
}
}
上面代码中,通过extends实现了Person类对People类的继承,并且在子类中出现了两处super。
- 构造函数中的super被当作函数运用,用于构建父类的this对象
- super当作对象使用,代表【父类原型对象】
【注意】子类必须在constructor方法中调用super方法,否则新建实例时会报错,这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。
ES5中的继承是先初始化子类,然后将父类的属性和方法添加到this上面(Parent.apply(this))。ES6的继承机制跟ES5有区别,ES6是先构建父类的this对象,然后再用子类的构造函数修改this。
如果子类没有显示定义constructor,这个方法会被默认添加,并调用super方法。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
【注意】在子类的构造函数中,只有调用了super之后才可以使用this关键字,否则会报错。因为子类实例的构建是基于父类实例加工的,只有super方法能返回父类实例。
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; // 正确
}
}
上面代码中,super调用之前使用this关键字报错,super之后使用this则正常。
2. 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作为对象时】
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 People { }
class Person extends People {
get m() {
return super.name
}
}
People.prototype.name = 'Blue';
let p = new Person();
p.m; //Blue
上面代码中,name属性在People的原型上,所以子类通过super是可以获取到的。
【ES6规定,通过super调用父类的方法时,super会绑定子类的this】
class People {
constructor() {
this.name = "Blue"
}
sayHi() {
console.log(this.name);
}
}
class Person extends People {
constructor() {
super();
this.name = "Crazy";
}
sayHi() {
super.sayHi();
}
}
let p = new Person();
p.sayHi(); //Crazy
上面代码中调用的是父类的sayHi( ),但是sayHi方法中的this是指向的子类的,所以输出的是 Crazy。
由于绑定的是子类的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
3.类的 prototype 属性和_proto_属性
大多数浏览器的 ES5 实现之中,每一个对象都有proto属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和proto属性,因此同时存在两条继承链。
- 子类的_proto_属性,表示构造函数的继承,总是指向父类。
- 子类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();
4. extends的继承目标
extends关键字后面可以跟多种类型的值。
class B extends A {
}
上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。
下面,讨论三种特殊情况。
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。
第二种特殊情况,不存在任何继承。
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype._proto_指向构造函数(Object)的prototype属性。
第三种特殊情况,子类继承null。
class A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true
这种情况与第二种情况非常像。A也是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回的对象不继承任何方法,所以它的_proto_指向Function.prototype,即实质上执行了下面的代码。
class C extends null {
constructor() { return Object.create(null); }
}
5. 原声构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
以前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。
ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。
class myArray extends Array {
constructor(...args) {
super(...args)
}
}
let arr = new myArray();
arr[0] = 1;
arr.length;//1
arr.length = 0;
arr[0];//undefined
上面的代码中我定义了myArray类,继承于Array构造函数,因此myArray拥有Array所有的属性和方法。
上面这个例子也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。下面就是定义了一个带版本功能的数组。
class VersionedArray extends Array {
constructor() {
super();
this.history = [[]];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0, this.length, ...this.history[this.history.length - 1]);
}
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]
x.revert();
x // [1, 2]
上面代码中,VersionedArray会通过commit方法,将自己的当前状态生成一个版本快照,存入history属性。revert方法用来将数组重置为最新一次保存的版本。除此之外,VersionedArray依然是一个普通数组,所有原生的数组方法都可以在它上面调用。
6. Mixin 模式的实现
Mixin 模式指的是,将多个类的接口“混入”(mix in)另一个类,我理解成多继承,它在 ES6 的实现如下。
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, mixin.prototype);
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}