15分钟深入了解JS继承分类、原理与用法
本文全面讲述了JS继承分类、原理与用法。分享给大家供大家参考,具体如下:
许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于ECMAScript中的函数没有签名,所以在JS中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。所以,下面所要说的原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承和寄生组合式继承都属于实现继承。
最后的最后,我会解释ES6中的extend语法利用的是寄生组合式继承。
1.原型链继承
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。实现原型链继承有一种基本模式,其代码大致如下:
functionSuperType(){ this.property=true; } SuperType.prototype.getSuperValue=function(){ returnthis.property; }; functionSubType(){ this.subproperty=false; } SubType.prototype=newSuperType();//敲黑板!这是重点:继承了SuperType SubType.prototype.getSubValue=function(){ returnthis.subproperty; }; varinstance=newSubType(); alert(instance.getSuperValue());//true
原型链继承的一个本质是重写原型对象,代之以一个新类型的实例;给原型添加方法的代码一定要放在替换原型的语句之后;在通过原型链实现继承时,不能使用对象字面量创建原型方法。
实例属性在实例化后,会挂载在实例对象下面,因此称之为实例属性。上面的代码中SubType.prototype=newSuperType();,执行完这条语句后,原SuperType的实例属性property就挂载在了SubType.prototype对象下面。这其实是个隐患,具体原因后面会讲到。
每次去查找属性或方法的时候,在找不到属性或方法的情况下,搜索过程总是要一环一环的前行到原型链末端才会停下来。
所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。由此可知,所有函数的默认原型都是object的实例,因此函数的默认原型都会包含一个内部指针,指向Object.prototype。
缺点:
- 最主要的问题来自包含引用类型值的原型。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
- 在创建子类型的实例时,不能向超类型的构造函数传递参数。
*题外话:确定原型与实例的关系的两种方式
- 第一种方式是使用instanceOf操作符,只要用这个操作符来测试实例的原型链中是否出现过某构造函数。如果有,则就会返回true;如果无,则就会返回false。以下为示例代码:
- 第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生出来的实例的原型。以下为示例代码:
alert(instanceinstanceofObject);//true alert(instanceinstanceofSuperType);//true alert(instanceinstanceofSubType);//true
alert(Object.prototype.isPrototypeOf(instance));//true alert(SuperType.prototype.isPrototypeOf(instance));//true alert(SubType.prototype.isPrototypeOf(instance));//true
2.借用构造函数继承
借用构造函数继承,也叫伪造对象或经典继承。其基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。其继承代码大致如下:
functionSuperType(){ this.colors=["red","blue","green"]; } functionSubType(){ SuperType.call(this);//敲黑板!注意了这里继承了SuperType } varinstance1=newSubType(); instance1.colors.push("black"); alert(instance1.colors);//"red,blue,green,black" varinstance2=newSubType(); alert(instance2.colors);//"red,blue,green"
通过使用call()方法(或apply()方法也可以),我们实际上是在(未来将要)新创建的子类的实例环境下调用父类构造函数。
为了确保超类构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
优点:可以在子类型构造函数中向超类型构造函数传递参数。
缺点:
- 方法都在构造函数中定义,每次实例化,都是新创建一个方法对象,因此函数根本做不到复用;
- 使用这种模式定义自定义类型,超类型的原型中定义的方法,对子类型而言是不可见。
3.组合继承
组合继承(combinationinheritance),有时候也叫做伪经典继承,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。其继承代码大致如下:
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();//继承方法 Object.defineProperty(SubType.prototype,"constructor",{//先继承,后定义新的自定义属性 enumerable:false,//申明该数据属性——constructor不可枚举 value: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();//29 varinstance2=newSubType("Greg",27); alert(instance2.colors);//"red,blue,green" instance2.sayName();//"Greg"; instance2.sayAge();//27
优点:
- 融合了原型链继承和借用构造函数继承的优点,避免了他们的缺陷;
- instanceOf()和isPrototypeOf()也能够用于识别基于组合继承创建的对象。
缺点:
在实现继承的时候,无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型的原型最终会包含超类型对象的全部实例属性,但我们不得不在定义子类型构造函数时重写这些属性,因为子类型的原型中最好不要有引用类型值。但这在实际中,就造成了内存的浪费。
4.原型式继承
原型式继承所秉承的思想是:在不必创建自定义类型的情况下,借助原型链,基于已有的对象创建新对象。这其中会用到Object.create()方法,让我们先来看看该方法的原理代码吧:
functionobject(o){ functionF(){} F.prototype=o; returnnewF(); }
从本质上讲,object()对传入其中的对象执行了一次浅复制。
ECMAScript5想通过Object.create()方法规范化原型式继承。这个方法接受两个参数:一参是被用来作为新对象原型的一个对象;二参为可选,一个为新对象定义额外属性的对象,这个参数的格式与Object.defineProperties()的二参格式相同。以下为原型式继承的示例代码:
varperson={ name:"Nicholas", friends:["Shelby","Court","Van"] }; varanotherPerson=Object.create(person,{ name:{ value:"Greg" } }); anotherPerson.friends.push("Rob"); alert(anotherPerson.name);//"Greg" varyetAnotherPerson=Object.create(person); yetAnotherPerson.name="Linda"; yetAnotherPerson.friends.push("Barbie"); alert(person.friends);//"Shelby,Court,Van,Rob,Barbie"
缺点:所有实例始终都会共享源对象中的引用类型属性值。
5.寄生式继承
寄生式(parasitic)继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。下面来看看,寄生式继承的示例代码:
functionobject(o){ functionF(){} F.prototype=o; returnnewF(); } functioncreateAnother(original){ varclone=object(original);//通过调用函数创建一个新对象 clone.sayHi=function(){//以某种方式来增强这个对象 alert("hi"); }; returnclone;//返回这个对象 }
该继承方式其实就是将原型式继承放入函数内,并在其内部增强对象,再返回而已。就相当于原型式继承寄生于函数中,故而得名寄生式继承。
前面示范继承模式时使用的object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。
缺点:不能做到函数复用,效率低下。
6.寄生组合式继承(推荐)
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。以下为寄生组合式继承的实例代码:
functionobject(o){ functionF(){} F.prototype=o; returnnewF(); } functioninheritPrototype(subType,superType){ varprototype=object(superType.prototype);//创建对象 prototype.constructor=subType;//增强对象 subType.prototype=prototype;//指定对象 } 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); };
优点:
- 只调用一次超类型构造函数;
- 避免了在子类原型上创建不必要的、多余的属性,节省内存空间;
- 原型链还能正常保持不变,也就意味着能正常使用instanceOf和isPrototypeOf()进行对象识别。
寄生组合式继承是最理想的继承方式。
7.ES6中的extend继承
来看看ES6中extend如何实现继承的示例代码:这一块的内容解释,我阅读的是这篇文章,欲知原文,请戳这里~
classChildextendsParent{ name='qinliang'; sex="male"; statichobby="pingpong";//staticvariable constructor(location){ super(location); } sayHello(name){ super.sayHello(name);//super调用父类方法 } }
我们再来看看babel编译过后的代码中的_inherit()方法:
function_inherits(subClass,superClass){ //SuperClass必须是一个函数,同时非null if(typeofsuperClass!=="function"&&superClass!==null){ thrownewTypeError("Superexpressionmusteitherbenullorafunction,not"+typeofsuperClass); } subClass.prototype=Object.create(//寄生组合式继承 superClass&&superClass.prototype,//原型上的方法、属性全部被继承过来了 { constructor:{//并且定义了新属性,这里是重写了constructor属性 value:subClass, enumerable:false,//并实现了该属性的不可枚举 writable:true, configurable:true } } ); if(superClass)//实现类中静态变量的继承 Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass; }
从这里我们就可以很明显的看出ES6中的extend语法,在内部实现继承时,使用的是寄生组合式继承。
下面我们来看看编译过后,除了_inherit()方法外的其他编译结果代码:
"usestrict"; var_createClass=function(){//利用原型模式创建自定义类型 functiondefineProperties(target,props){//对属性进行数据特性设置 for(vari=0;i从我的注释中就可以看出_possibleConstructorReturn()函数,其实就是寄生组合式继承中唯一一次调用超类型构造函数,从而对子类型构造函数进行实例化环境的初始化。从这点,我们可以更加确定的ES6中的extend使用的是寄生组合式继承。
更多关于JavaScript相关内容还可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。