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

作者: Web前端学习营 | 来源:发表于2019-06-15 16:01 被阅读2次

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

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

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

    原型链

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

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

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

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

    JS code:functionSuperType(){this.property =true;};SuperType.prototype.getSuperValue =function(){returnthis.property;};functionSubType(){this.subproperty =false;}// 继承了SuperTypeSubType.prototype =newSuperType();SubType.prototype.getSubValue =function(){returnthis.subproperty;};varinstance =newSubType();alert(instance.getSuperValue());// true复制代码

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

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

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

    JS code:functionSuperType(){this.colors = ["red","blue","green"];}functionSubType(){}SubType.prototype =newSuperType();//继承了 SuperTypevarinstance1 =newSubType(); instance1.colors.push("black"); alert(instance1.colors);//"red,blue,green,black"varinstance2 =newSubType(); alert(instance2.colors);//"red,blue,green,black"复制代码

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

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

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

    JS code:functionSuperType(){this.colors = ["red","blue","green"];    }functionSubType(){//继承了 SuperTypeSuperType.call(this);}varinstance1 =newSubType();instance1.colors.push("black");alert(instance1.colors);//"red,blue,green,black"varinstance2 =newSubType();alert(instance2.colors);//"red,blue,green"复制代码

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

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

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

    组合继承 [伪经典继承]

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

    JS code:functionSuperType(name){this.name = name;this.colors = ["red","blue","green"];}SuperType.prototype.sayName =function(){    alert(this.name);};functionSubType(name, age){//继承属性 SuperType.call(this, name);this.age = age;};//继承方法SubType.prototype =newSuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge =function(){    alert(this.age);};varinstance1 =newSubType("Nicholas",29);instance1.colors.push("black");alert(instance1.colors);//"red,blue,green,black"instance1.sayName();//"Nicholas";instance1.sayAge();//29varinstance2 =newSubType("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:functionobject(o){functionF(){};    F.prototype = o;returnnewF();}复制代码

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

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

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

    这个方法接收两个参数

    用作新对象原型的对象和(可选的)

    一个为新对象定义额外属性的对象。

    在传入一个参数的情况下, Object.create()与 object()方法的行为相同。 

    JS code:varperson = {    name:"Nicholas",    friends: ["Shelby","Court","Van"]};varanotherPerson =Object.create(person);anotherPerson.name ="Greg";anotherPerson.friends.push("Rob");varyetAnotherPerson =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:functioncreateAnother(original){varclone= object(original);//通过调用函数创建一个新对象clone.sayHi =function(){//以某种方式来增强这个对象alert("hi");};returnclone;//返回这个对象}varperson = {    name:"Nicholas",    friends: ["Shelby","Court","Van"]};varanotherPerson = createAnother(person);anotherPerson.sayHi();//"hi"复制代码

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

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

    寄生组合式继承

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

    以下是组合式继承:

    JS code:functionSuperType(name){this.name = name;this.colors = ["red","blue","green"];}SuperType.prototype.sayName =function(){    alert(this.name);};functionSubType(name, age){    SuperType.call(this, name);// 第二次调用SuperType()this.age = age;}SubType.prototype =newSuperType();// 第一次调用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:functioninheritPrototype(subType, superType){varprototype = object(superType.prototype);// 创建对象prototype.constructor = subType;// 增强对象subType.prototype = prototype;// 指定对象}复制代码

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

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

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

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

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

    JS code:functionSuperType(name){this.name = name;this.colors = ["red","blue","green"];}SuperType.prototype.sayName =function(){    alert(this.name);};functionSubType(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继承,欢迎大神留言填充。

    相关文章

      网友评论

        本文标题:混子前端所知道关于JS的六种继承方式

        本文链接:https://www.haomeiwen.com/subject/dmvbfctx.html