面向对象实现继承的几种方法
1、原型链
1.1、构造函数和原型和实例之间的关系
每个构造函数都有一个原型对象,原型对象又有一个指向构造函数的指针,实例又有一个指向原型对象的指针。
1.2、原型链的概念
假如我们让原型对象等于另一个类型的实例,类型我理解为不同的构造函数,此时的原型对象将包含另一个原型对象的指针,相应的,另一个原型中也包含一个指向另一个构造函数的指针,假如,另一个原型是又另外一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。
function SuperType(){ this.property=true; } SuperType.prototype.getSuperValue=function(){ console.log(this.property); } function SubType(){ this.subProperty=false; } SubType.prototype=new SuperType(); //继承了SuperType SubType.prototype.constructor=SubType; SubType.prototype.getSubValue=function(){ console.log(this.subProperty); } var o = new SubType(); o.getSuperValue(); //true
以上代码定义了2个类型,SuperType和SubType,每个类型分别有一个类型和一个方法,他们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例并将该实例赋给SubType的原型实现的。实现的本质是重写原型对象,代之以一个新类型的实例,原来存在于SuperType的实例的属性和方法,现在也存在于SubType.prototype中了。
注意:加背景那一行在继承了SuperType类型之后,立马更正了SubType的原型的constructor属性,因为,此时constructor属性已经指向了SuperType。
问题:原型链虽然很强大,可以实现继承,但面对引用类型值的原型,有一个问题。
function SuperType(){ this.colors=["red","blue","green"]; } function SubType(){ } SubType.prototype=new SuperType(); var o1=new SubType(); o1.colors.push("pink"); console.log(o1.colors); //["red", "blue", "green", "pink"] var o2=new SubType(); console.log(o2.colors); //["red", "blue", "green", "pink"]
以上代码,superType构造函数定义了一个colors属性,该属性包含一个数组(引用类型值),SuperType的每个实例都会有各自包含数组的colors数组。当SubType 通过原型链继承了SuperType 之后,SubType.prototype 就变成了SuperType 的一个实例,因此它也拥有了一个它自己的colors 属性,就跟专门创建了一个SubType.prototype.colors 属性一样。但结果是什么呢?结果是SubType 的所有实例都会共享这一个colors 属性。
2、借用构造函数----伪造对象----经典继承
由于原型中包含引用类型的值有问题,又有一种新的方法出来了,叫做借用构造函数,或者伪造对象,或者经典继承,在子类型构造函数的内部调用超类型构造函数。
function SuperType(){ this.colors=["red","green"]; } SuperType.prototype.sayHi=function(){ alert("hello~"); } function SubType(){ SuperType.call(this); } var o=new SubType(); o.colors.push("pink"); console.log(o.colors); //["red", "green", "pink"] var o1=new SubType(); console.log(o1.colors); //["red", "green"] o1.sayHi(); //报错
以上代码中加背景的那一行调用了超类型的构造函数,通过使用call方法,我们实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数,这样的话,SubType的每个实例都会具有自己的colors属性的副本了。
2.1、借用构造函数的优势-----传递参数
相对于原型链来说,借用构造函数有一个优势就是,可以传递参数,即可以在子类型构造函数中向超类型构造函数传递参数。
function SuperType(name){ this.name=name; } function SubType(){ SuperType.call(this,"张三"); this.age=24; } var o=new SubType(); alert(o.name); //张三 alert(o.age); //24
2.2、借用构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题----方法都在构造函数中定义,因此函数的复用也就无从谈起了,而且在超类型的原型中定义的方法,对子类型而言也是不可见的。
3、组合继承----伪经典继承----较为常用的继承方法
组合继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式,思路是使用原型链实现原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,这样的话,即通过在原型上定义方法实现了函数的复用,又能保证每个实例都有他自己的属性。
function SuperType(name){ this.name=name; this.colors=["red","green"]; } SuperType.prototype.sayName=function(){ console.log(this.name); } function SubType(name,age){ SuperType.call(this,name); //继承了属性 this.age=age; } SubType.prototype=new SuperType(); //继承了方法 SubType.prototype.constructor=SubType; //将SubType的原型的constructor属性指回SubType SubType.prototype.sayAge=function(){ console.log(this.age); } var o=new SubType("张三",23); o.colors.push("pink"); console.log(o.colors); //["red", "green", "pink"] o.sayName(); //张三 o.sayAge(); //23 var o1=new SubType("李四",25); console.log(o1.colors); //["red", "green"] o1.sayName(); //李四 o1.sayAge(); //25
以上代码中SuperType定义了2个属性name和colors,SuperType的原型定义了一个方法sayName(),SunType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了它自己的属性age,然后,将SuperType的实例赋值给SubType的原型,然后,又在该新原型上定义了方法sayAge,这样一来,就可以让俩个不同的SubType实例既分别拥有自己的属性,包括colors属性,又可以使用相同的方法了,组合继承是javascript中最常用的继承模式。instanceof 和isPrototypeOf()也能够用于识别基于组合继承创建的对象。
4、原型式继承
这种方法不使用构造函数实现继承,由json格式的发明人Douglas Crockford,提出了一个object()函数。
function object(o){ function F(){}; F.prototype=o; return new F(); }
以上代码,在object函数的内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。
function object(o){ function F(){}; F.prototype=o; return new F(); } var person={ name:"张三", colors:["red","green"] } var o1=object(person); o1.name="李四"; o1.colors.push("pink"); console.log(o1.colors); //["red", "green", "pink"] var o2=object(person); console.log(o2.colors); //["red", "green", "pink"]
这种原型式继承,要求你必须有一个对象作为另一个对象的基础,如果有那么一个对象的话,可以把它传递给object函数,然后再根据具体需求对得到的对象加以修改即可。
ECMAScript 5,通过新增Object.create()方法规范化了原型式继承,这个方法接收2个参数,一个用作新对象原型的对象,和(可选的)一个为新对象定义额外属性的对象,在传入一个参数的情况下,Object.create()和上面的object()方法的行为相同。
var Chinese={ nation:"中国" } var Doctor={ job:"医生" } var o=Object.create(Chinese); o.job="医生"; console.log(o.nation); //中国
Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:
var Chinese={ nation:"中国" } var Doctor={ job:"医生" } var o=Object.create(Chinese,{ job:{ value:"医生" } }); console.log(o.job); //医生
支持Object.create()方法的浏览器有IE9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。
只想让一个对象和另一个对象保持类似的情况下,原型式继承是可以的。
5、寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像是真的是它做了所有工作一样返回对象。
function Object(o){ function F(){} F.prototype=o; return new F(); } function createAnother(original){ var clone=Object(original); clone.sayHi=function(){ //以某种方式来增强这个对象 console.log("hi~"); } return clone; } var person={ name:"张三", colors:["red","green"] } var anotherPerson=createAnother(person); anotherPerson.sayHi(); //hi~
以上例子中的代码,基于person返回了一个新对象,anotherPerson,新对象不仅具有person的所有属性和方法,还有属于自己的sayHi方法。在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。
6、继承组合式继承----最为理想的继承方法
组合继承是javascript最常用的继承模式,但是,它最大的问题就是无论什么情况下,都会调用2次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部,虽然子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性,回顾一下组合继承。
function SuperType(name){ this.name=name; this.colors=["red","green"]; } SuperType.prototype.sayName=function(){ console.log(this.name); } function SubType(name,age){ SuperType.call(this,name); //第二次调用了SuperType this.age=age; } SubType.prototype=new SuperType(); //第一次调用了SuperType SubType.prototype.sayAge=function(){ console.log(this.age); } var o1=new SubType("张三",23); o1.colors.push("pink"); console.log(o1.colors); //["red", "green", "pink"] var o2=new SubType("李四",25); console.log(o2.colors); //["red", "green"]
以上代码,第一次调用SuperType构造函数时,SubType.prototype会得到俩个属性,name和colors,它们都是SuperType的实例属性,只不过现在位于SubType的原型中,当调用SubType的构造函数时,又会调用一次SuperType构造函数,这一次,又在新对象上创建了实例属性,name和colors,于是,这俩个实例属性就屏蔽了原型中的俩个同名属性。
寄生组合式继承的基本模式如下:
function inheritPrototype(SubType,SuperType){ var prototype=Object(SuperType.prototype); //创建对象 prototype.constructor=SubType; //增强对象 SubType.prototype=prototype; //指定对象 }
以上自定义函数中,接收2个参数,子类型构造函数和超类型构造函数,在函数内部第一步,创建超类型原型的一个副本,第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性,最后一步,将新创建的对象(即副本)赋值给子类型的原型,以下,将这个函数运用于前面的例子:
function Object(o){ function F(){} F.prototype=o; return new F(); } function inheritPrototype(SubType,SuperType){ var prototype=Object(SuperType.prototype); //创建对象 prototype.constructor=SubType; //增强对象 SubType.prototype=prototype; //指定对象 } function SuperType(name){ this.name=name; this.colors=["red","green"]; } SuperType.prototype.sayName=function(){ console.log(this.name); } function SubType(name,age){ SuperType.call(this,name); this.age=age; } inheritPrototype(SubType,SuperType); SubType.prototype.sayAge=function(){ console.log(this.age); } var o1=new SubType("张三",23); o1.colors.push("pink"); console.log(o1.colors); //["red", "green", "pink"] var o2=new SubType("李四",25); console.log(o2.colors); //["red", "green"]
以上代码,高效率体现在它只调用了一次SuperType构造函数,并且,因此避免了在SubType.prototype上创建不必要的,多余的属性,现在SubType.prototype上面只有sayName方法和sayAge方法了,寄生组合式继承是引用类型最为理想的继承方式。