愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

《ECMAScript6入门》学习笔记之Class的继承

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)
        )
    });
}