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

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

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


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

原型链

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

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

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

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

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 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。 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继承,欢迎大神留言填充。


最后祝你们周末愉快 !

相关文章
相关标签/搜索