混子前端所知道关于JS的六种继承方式

1,486 阅读9分钟

继承是OO语言中的一个最为人津津乐道的概念。许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。

---本文摘选《JavaScript高级程序设计(第3版)》


由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScirpt只支持实现继承,而且其实现继承主要依靠原型链来实现的。

原型链

ECMAScript中描述了原型链作为实现继承的主要方法。

基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。

PS:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

如果让原型对象等于另一个类型的实例,此时原型对象将包含一个指向另一个原型的指针,另一个原型中也包含着一个指向另一个构造函数的指针。另一个原型又是另一个类型的实例,如此层层递进,就构成了实例与原型的链条。

JS code:

function SuperType(){
    this.property = true;
};
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();
alert(instance.getSuperValue());    // true

以上代码定义了两个类型:SuperType 和 SubType。每个类型分别有一个属性和一个方法。它们 的主要区别是 SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋给 SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。 

注意:instance.constructor 现在指向的 是 SuperType,这是因为原来 SubType.prototype 中的 constructor 被重写了的缘故。

不足通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。 下面代码可以说明这个问题:

JS code:

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

function SubType(){
}

SubType.prototype = new SuperType();    //继承了 SuperType

var instance1 = new SubType(); 
instance1.colors.push("black"); 
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"

PS:在创建子类型的实例时,不能向超类型的构造函数中传递参数;这也是原型链继承的不足。 


借用构造函数 [伪造对象或经典继承]

基本思想:在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过使用 apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数。

JS code:

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

function SubType(){    
    //继承了 SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors);    //"red,blue,green"

以上代码通过使用 call()方法[ 或 apply()方法 ],我们实际上是在新创建的 SubType 实例的环境下调用了 SuperType 构造函数。 这样就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。结果, SubType 的每个实例就都会具有自己的 colors 属性的副本了。 

PS:相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数,这里就不举例说明,只是call()方法[ 或 apply()方法 ]的特性。

不足:方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。 


组合继承 [伪经典继承]

基本思想:将原型链和借用构造函数的 技术组合到一块,从而发挥二者之长的一种继承模式。背后思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。 

JS code: 

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 = new SuperType(); 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"
instance1.sayName();    //"Nicholas";
instance1.sayAge();    //29

var instance2 = new SubType("Greg", 27); 
alert(instance2.colors);    //"red,blue,green"
instance2.sayName();    //"Greg";
instance2.sayAge();    //27

SuperType 构造函数定义了两个属性:name 和 colors。SuperType 的原型定义了一个方法 sayName()。SubType 构造函数在调用 SuperType 构造函数时传入了 name 参数,紧接着又定义了它自己的属性 age。然后将 SuperType 的实例赋值给 SubType 的原型,然后又在该新原型 上定义了方法 sayAge()。这样一来,就可以让两个不同的 SubType 实例既分别拥有自己属性——包括 colors 属性,又可以使用相同的方法了。 

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。 


原型式继承 

基本思想:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。参考如下函数:

JS code:

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。 

PS:这种原型式继承要求必须有一个对象可以作为另一个对象的基础。如果有这么 一个对象的话,可以把它传递给 object()函数,然后再根据具体需求对得到的对象加以修改即可。 

而ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。

这个方法接收两个参数

  1. 用作新对象原型的对象和(可选的)
  2. 一个为新对象定义额外属性的对象。
在传入一个参数的情况下, Object.create()与 object()方法的行为相同。 

JS code:

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends);    //"Shelby,Court,Van,Rob,Barbie"

PS:Object.create()方法的第二个参数与 Object.defineProperties()方法的第二个参数格式相

同,这里不做解释。

适用场景:只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的,不过包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。 


寄生式继承 

基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。 

JS code:

function createAnother(original){ 
    var clone = object(original);    //通过调用函数创建一个新对象
    clone.sayHi = function(){    //以某种方式来增强这个对象
    alert("hi");
};
    return clone;    //返回这个对象
}

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();    //"hi"

基于 person 返回了一个新对象 [ anotherPerson ];新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi()方法。 

适用场景:在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。 


寄生组合式继承 

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

以下是组合式继承:

JS code:

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);    // 第二次调用SuperType()
    this.age = age;
}
SubType.prototype = new SuperType();    // 第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};

第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性:name 和 colors;它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中;

当调用 SubType 构造函数时,会第二次调用SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors

于是,这两个属性就屏蔽了原型中的两个同名属性。 

[寄生组合式继承]基本思想:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。 

JS code:function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);    // 创建对象
    prototype.constructor = subType;    // 增强对象
    subType.prototype = prototype;  // 指定对象
}

这个函数接收两个参数:子类型构造函数 和 超类型构造函数。

在函数内部,第一步是创建超类型原型的一个副本。

第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。 

最后一步,将新创建的对象(即副本)赋值给子类型的原型。

这样,我们就可以用调用 inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了。

JS code:

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(SubType, SuperType);

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

PS:寄生组合式继承高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要 多余的属性,同时,原型链还能保持不变;

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


ESMA5 的继承可以用下图来概括:


好了,以上就是混子前端所知道 ECMA5 中的六种继承方式,这里省略 ECMA6 中class继承,欢迎大神留言填充。


最后祝大家周末愉快 !