梳理篇之JavaScript 继承

425 阅读4分钟

概述

  1. 为什么需要继承?

    面向对象编程主要目标:复用性、灵活性和扩展性。复用即一些属性或者方法可以被多个对象实例共享。JavaScript中所有引用类型的数据类型都会继承 Object ,如果没有继承,意味着每个数据类型都需要自行实现一些基础的方法,例如 toString。

  2. 常见的继承方式?

    继承一般有:接口继承、实现继承。接口继承只继承方法的签名;实现继承则继承实际的方法。而Javascript中的使用的就是实现继承。

    关于继承的讨论可以参考:

    面向对象编程的弊端是什么? - invalid s的回答 - 知乎

    为什么阿里巴巴建议开发者谨慎使用继承?

    React 101 - Composition vs Inheritance

JavaScript的基础方式

1. 原型继承

核心问题:原型继承的原理是什么?

借助原型链概念,实现属性和方法的继承:

function SuperType(){
    this.property = true;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
//继承了 SuperType, 核心点
SubType.prototype = new SuperType(); 
SubType.prototype.getSubValue = function (){
    return this.subproperty;
  };
var instance = new SubType();
instance.colors.push("black");
alert(instance.getSuperValue()); //true

var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black" // 引用类型已经被修改

原型链继承即是子函数的原型为父函数的实例,本质上扩展了对象的原型搜索机制

缺陷

最主要的问题来自包含引用类型值的原型。包含引用类型值的原型属性会被所有实例共享;而 这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。通过原型来实现继承时,原 型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性。

其次在创建子类型的实例时,不能向超类型的构造函数中传递参数。

2. 组合继承

核心问题: 既然是组合,那组合了哪些内容?

组合继承指的是将原型继承 和 **构造函数借用(constructor stealing)**组合到一块实现属性和方法的继承

构造函数借用:是指通过call 或者 apply 在子函数中调用父函数

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){
    //借用 SuperType
    SuperType.call(this);
}

使用组合继承,通过借用构造函数,结合原型属性屏蔽特征,有效解决了原型继承的缺点。但存在一个问题:

无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是 在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子 类型构造函数时重写这些属性。

3. 寄生式继承

核心问题:寄生式继承,谁寄生,怎么寄生的?

回顾一下原型链继承,子函数需要使用 SubType.prototype = new SuperType(), 导致SuperType属性和原型上的方法都存在于 SubType 原型了,本意应该只是继承父函数的原型。也就是说 SubType.prototype 应该只与 SuperType.prototype 的有关系就好了。

实现方式可以有:

SubType.prototype =  Object.create(SuperType.prototype)

// 或者使用其适配
function createObject(prototype) {
    function F() {}
    F.protoytpe = prototype
    return new F()
}
SubType.prototype =  createObject(SuperType.prototype)

这样就是实现父子函数的原型的桥接,也就是这里体现的所谓的寄生?:需要被继承的对象寄生于一个被创建的对象上,看以下示例可能更好理解些:

function createParasiticObject(parent) {
    const child = Object.create(parent)
    child.sayHi = function () {
        alert('hi')
    }
    return child
}

const original = {
  sayHello() {console.log('hello')}
}
const instance = createParasiticObject(original)
instance.sayHi()
instance.sayHello()

也就是被继承的对象original 通过原型的方式直接寄生到 instance 上面了,该过程和构造函数区别应该就是不需要使用 new 来生成一个对象了吧。 好难理解。。强行解释了一波。

4. 组合寄生式继承

这个就比较好理解了,主要就是解决组合继承中存在的两次构造函数的调用问题,即原型通过寄生式继承完成,然后组合 借用构造函数 方式完成对属性的继承。

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;
}
SubType.prototype = Object.create(SuperType.prototype);

SubType.prototype.sayAge = function(){
    alert(this.age);
};

总的来说寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。

以上总体概念以及相关介绍来源于《JavaScript高级程序设计》第三版,重新阅读还是有些新的收获