实现继承的几种方式

实现继承一般有两种方式:接口继承和实现继承。
因为JavaScript中函数没有签名(什么是函数(方法)签名,为什么JS函数没有签名?
),所以只能以实现继承的方式来完成继承。这一操作主要依靠原型链实现。
在JavaScript中实现继承的方式主要有: 原型链 借用构造函数 组合继承 原型式继承 寄生式继承 寄生组合式继承 这几种。同上文提到的创建对象方式类似,原型链 借用构造函数 组合继承这三种是较为常见的方式,又以 借用构造函数 最为常用。

原型链继承

    // 定义超类的属性和方法
    const SuperType = function () {
        this.name = 'father';
        // 声明一个引用类型的属性
        this.childList = ['childInstance', 'childInstance2'];
    };
    SuperType.prototype.getName = function () {
        console.log(this.name);
    };
    // 定义子类
    const ChildType = function () {
        this.childName = 'child';
    };
    // 将子类的原型对象指向超类的实例,从而实现对超类的继承
    ChildType.prototype = new SuperType();
    // 定义自己的方法
    ChildType.prototype.getChildName = function () {
        console.log(this.childName);
    };
    // 创建子类实例
    let childInstance = new ChildType();
    // 访问子类原型对象上的方法
    childInstance.getChildName();
    // 通过原型链访问继承超类而来的方法
    childInstance.getName();
    /* 同创建对象的原型类似,通过这种方式实现的继承存在如下问题
    *  当其中一个实例访问并修改原型对象中的引用类型值时,会修改原型对象上的值,从而导致别的实例对这个值的访问也变化了
    * */
    let childInstance2 = new ChildType();
    console.log(childInstance2.childList);  // ["childInstance", "childInstance2"]
    childInstance.childList.push('childInstance3');
    console.log(childInstance2.childList); // ["childInstance", "childInstance2", "childInstance3"]

借用构造函数

    // 定义超类的属性和方法,同上
    const SuperType = function (name) {
        this.name = name;
        // 声明一个引用类型的属性
        this.childList = ['childInstance', 'childInstance2'];
    };
    // 定义子类及子类和父类的继承关系
    const ChildType = function (name = '') {
        /* 通过call/apply指定超类的运行环境,从而将绑定在原型对象上的属性绑定到各个子类的实例上
        *  解决原型链继承存在的共享原型对象属性的问题,并且可以向超类中传参
        * */
        SuperType.call(this, name);
    };

    // 生成子类实例
    let childInstance = new ChildType('instance1');
    let childInstance2 = new ChildType('instance2');
    /* 在子类构造函数中指定了构造函数里的 this 指向为各个实例,
    *  即使是在一个实例中对超类构造函数中定义的引用类型做修改也不会影响到别的实例
    *  因为现在各个属性都是挂载在实例下而不是在原型对象下,不同实例相同名字的属性初始值虽然相同但是是完全独立的
    * */
    console.log(childInstance2.childList);
    childInstance.childList.push('childInstance3');
    console.log(childInstance2.childList);

组合继承

组合继承同创建对象的 组合使用构造函数模式和原型模式 方式思想类似,兼顾了属性的独立和方法的复用。

    // 定义父类构造函数和属性,同上
    const SuperType = function (name) {
        this.name = name;
        // 声明一个引用类型的属性
        this.childList = ['childInstance', 'childInstance2'];
    };
    // 通过原型模式定义父类的方法,使之能被子类复用
    SuperType.prototype.getName = function () {
        console.log(this.name);
    };
    // 定义子类及其和父类的继承关系
    const ChildType = function (name) {
        // 指定构造函数的执行环境实现属性继承
        SuperType.call(this, name);
    };
    // 将原型对象指向超类实例实现方法继承
    ChildType.prototype = new SuperType();
    // 此时基于子类创建的实例的constructor指向父类构造函数,因此补上constructor指向
    ChildType.prototype.constructor = ChildType;
    // 生成子类实例
    let childInstance = new ChildType('instance1');
    let childInstance2 = new ChildType('instance2');
    // 通过原型链访问超类的方法
    childInstance.getName();
    childInstance2.getName();

以上三种就是比较常见的继承方式,书本还介绍了额外几种继承方式。此处只介绍 寄生组合式继承。因为

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

寄生组合式继承解决的一大问题便是组合继承中对超类构造函数的两次调用,这个的实现是借助于直接在一个新的对象中完成实现子类和超类的继承关系来达成的。请看书本代码:

// 该方法实现了原型式继承,封装了 创建一个传入对象的子类构造函数 的功能。
function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create() 与 object() 方法的行为相同。

// 封装实现寄生组合式继承的过程
function inheritPrototype(subType, superType){
    // 创建一个超类原型对象的副本
    var prototype = object(superType.prototype); //创建对象
    // 将constructor的指向改为subType,因为这个函数是对subType做增强
    prototype.constructor = subType; //增强对象
    // 实现subType和superType之间的继承关系
    subType.prototype = prototype; //指定对象
}

代码使用如下

// 如之前定义超类构造函数一致
function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
// 如上文所说,实现对超类属性的继承
function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}
/*原来此处是将子类的原型对象指向超类的实例,这样又会产生一次对超类的调用
* 此处使用寄生组合式继承的方式,将对超类的调用转为指向超类原型对象的一个副本
* 然而 inheritPrototype方法 在 object() 实现原型式继承的方法中又做了一次创建以superType为超类的构造函数的实例过程,虽然减少了一次对superType的调用但并没有减少对象实例化次数的操作
* 说白了就是用在 object() 方法中一次实例化操作替代了对SuperType的实例化操作
**/
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
    alert(this.age);
};

在ES6中增加了class的概念,简化了定义父类子类、实现继承的过程。

    // 定义一个超类
    class SuperType {
        // 类方法内部的this指向实例
        constructor(name) {
            this.name = name;
        }

        // 定义方法,类的所有方法都定义在类的prototype属性上面
        getName() {
            console.log(this.name);
        }
    }

    // 挂载在超类的原型对象下使得子类都能访问到
    SuperType.prototype.childList = ['childInstance', 'childInstance2'];

    // 定义一个继承自超类的子类,使用 extends 关键词替代原来的修改prototype指向
    class ChildType extends SuperType {
        constructor(name) {
            // 调用超类的constructor
            super(name);
        }
    }

    // 生成子类实例
    let childInstance = new ChildType('instance1');
    let childInstance2 = new ChildType('instance2');
    // 访问各个实例下的属性
    childInstance.getName();
    childInstance2.getName();
    // 访问挂载在原型对象下的引用类型值
    console.log(childInstance2.childList);
    childInstance.childList.push('childInstance3');
    console.log(childInstance2.childList);

补充一些容易混淆的概念。
关于prototype__proto__:

  • prototype是函数的一个属性,指向函数的原型对象
  • __proto__是浏览器为基于构造函数创建的实例实现 [[Prototype]] 指针的一种方式,指向构造函数的原型对象,只存在于实例和构造函数的原型对象之间
    所以上文实现中有
    new SuperType().__proto__ === SuperType.prototype  // true

而对于class而言既有prototype属性也有__proto__属性,有如下逻辑:
子类的__proto__属性,表示构造函数的继承,总是指向父类。
子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

    ChildType.__proto__ === SuperType  // true
    ChildType.prototype.__proto__ === SuperType.prototype  // true

通过以上代码可以看出,class语法只是对组合继承方式的一种封装,提供了更方便的使用方式;它依旧保留着子类和超类之间的原型链关系,超类和子类的数据类型依然是function。更多语法可以查看 阮一峰 ES6入门 class