经过前三节的研究,我们终于可以对js这门无法实现接口继承的语言进行实现继承,为了更好的面向对象。。。。。
原型链继承
这个原型链我们上节引用了一下,用它实现继承的基本思想是利用原型让一个引用类型引用另一个引用类型的属性和方法。即把一个函数(狭义上来举例)的所有属性和方法(这个函数的实例)赋值给另一个函数的prototype,使一个函数的实例可以通过__proto__原型一层层的调用另一个函数的所有属性。
有点绕,简单的说就是把超类的实例赋值给子类的prototype。看个例子:
1 function SuperType () { 2 this.name = 'super'; 3 }; 4 SuperType.prototype.getName = function() { 5 console.log(this.name); 6 }; 7 8 function SubType () { 9 this.age = 10; 10 }; 11 SubType.prototype=new SuperType(); 12 SuperType.prototype.getAge = function() { 13 return this.age; 14 }; 15 16 var sub = new SubType(); 17 sub.getName(); //super
如上第11行为将SuperType的实例赋值给SubType的prototype,那么SubType的实例化对象sub便可以访问父类的方法getName(),因为这个方法已经存在于sub的原型上。
但是原型链也不是没自己的问题,最主要的问题来自包含引用类型值的原型属性会被所有实例共享。原型实际上会变成另一个类型的实例,于是,原先的实例属性就变成了现在的原型属性了。
1 function SuperType () { 2 this.family=['brother','sister']; 3 }; 4 5 function SubType () {}; 6 SubType.prototype = new SuperType(); 7 8 var obj1=new SubType(); 9 obj1.family.push('cousin') 10 console.log(obj1.family); //["brother", "sister", "cousin"] 11 12 var obj2=new SubType(); 13 console.log(obj2.family); //["brother", "sister", "cousin"]
第6行将SuperType的实例化赋值给SubType的原型后,就相当于创建了哟个SubType.prototype.family属性一样,结果就是SubType所有的实例有共享这个属性,造成了通过obj1修改这个属性在obj2上也反应出来。
借用构造函数
这种方式的思想是在子类构造函数的内部调用超类构造函数,也就是说通过call()或者apply()方法在新创建的SubType实例的环境下调用了SuperType构造函数,再通俗点这个新创建的对象是在借胎生子,看似是SubType的孩子,实质上是SuperType的孩子,当然可以继承两个人所有可继承的属性和方法。
1 function SuperType () { 2 this.family = ['brother','sister']; 3 }; 4 function SubType () { 5 SuperType.call(this); 6 }; 7 var obj1 = new SubType(); 8 obj1.family.push('cousin'); 9 console.log(obj1.family); //["brother", "sister", "cousin"] 10 11 var obj2 = new SubType(); 12 console.log(obj2.family); //["brother", "sister"]
如上在第7行实例化obj1的时候,调用SubType的构造函数,而SubType的内在实际上又执行了Supertype的构造函数,所以实现了继承,而obj2会在实例化的时候再次调用SuperType构造函数,重新得到一个SuperType.prototype.family,同时call()和apply()函数都是可以传参的,完美解决上述原型链继承的问题。在此就不演示传参了。
但是借用构造器就没问题吗,当然不,其实和创建对象使用的构造函数模式的问题是一样的——所有的方法必须写在构造函数里,那么这样每次都会实例化一次方法,这样做的后果就是函数无法实现复用,不符合面向对象的思想。
组合继承
这种方式其实是让上述两种方式取长补短,使用原型链实现对原型属性和方法的继承,使用构造函数实现实例属性的继承,这样既保证函数的复用,又可以保证每个实例都有自己独有的属性。
1 function SuperType (name) { 2 this.name=name; 3 this.color=['red','blue'] 4 }; 5 SuperType.prototype.sayName=function () { 6 console.log(this.name); 7 }; 8 function SubType (name) { 9 SuperType.call(this,name); 10 }; 11 SubType.prototype=new SuperType(); 12 SubType.prototype.constructor = SubType; 13 14 var obj1 = new SubType('James'); 15 obj1.color.push('black'); 16 console.log(obj1.color); //['red','blue','black'] 17 obj1.sayName(); //James 18 19 var obj2 = new SubType('Wade'); 20 console.log(obj2.color); //['red','blue'] 21 obj2.sayName(); //Wade
第9行为借用构造函数,保证了实例化的时候每次都会取一个新的name和color,而原型上的方法可以实现该方法的共享,保证复用性,第12行属于对原型构造的重定向,因为11行的时候SubType的所有属性均来自SuperType,其中自然也包括这个constructor。一句话总结:原型继承太热衷于共享,把应该共享的(方法)和不应该共享的(有个性的实例属性)通通共享,构造函数法太热衷于个性,把应该共享的都独自分配了。这种组合继承方法融合了他们优点,已经成为最常用的继承模式。
原型式继承
其实就是把上边的组合式继承的构造函数过程进行了封装,先看看代码:
1 function object (o) { 2 function F(){}; 3 F.prototype=o; 4 return new F(); 5 }
首先说这个个临时中转函数,在object函数的内部,传入了一个对象,将这个对象作为这个构造函数的原型,最后返回这个临时函数的新实例,这个新的实例具有对象o的所有的属性和方法。
但是这个F.prototype=o;语句会共享这个对象o,和原型链继承遇到的问题是一样的,这时候就得说一下我们以前的一个老朋友Object.create()方法,这个方法接受两个参数,第一个参数用作新对象原型的对象和一个为新对象定义额外属性的对象(可选)。在只传递一个参数的情况下,这个方法和上述object是一样的。所以也没有克服包含引用类型值的属性共享的问题,但可以提供一个完美的方案,往下看。
寄生式继承
这种方式会创建一个用于封装继承过程的临时中转函数,在该函数的内部做了某些操作后返回这个对象,如下代码:
寄生组合式继承
前边谈过,组合式继承是最常用的一种继承模式,但也有自己的问题,他在任何情况下都会调用两次构造函数,回到上边,第一次在第11行创建子类型的原型时new了一下,第二次在第9行子类型构造函数的内部,所以目前最完美的解决方案就出来了,就是这个寄生组合式继承:
这种方式实质上是用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型,相当于对Subtype.prototype=new Super();的另一种操作。首先又是一个临时中转函数:
1 function inheritProtoType (subType,superType) { 2 var prototype = Object.create(superType).prototype; 3 prototype.constructor = subType; 4 subType.prototype=prototype; 5 };
用这个函数的调用代替组合继承中的Subtype.prototype=new Super();,则可以得到:
1 function inheritProtoType (subType,superType) { 2 var prototype = Object.create(superType).prototype; 3 prototype.constructor = subType; 4 subType.prototype=prototype; 5 }; 6 7 function SuperType (name) { 8 this.name = name; 9 this.color = ['blue','red']; 10 }; 11 SuperType.prototype.sayName=function () { 12 console.log(this.name); 13 }; 14 function SubType(name){ 15 SuperType.call(this,name); 16 }; 17 inheritProtoType(SubType,SuperType); 18 var obj = new SubType('success'); 19 obj.sayName(); //success 20 obj.color.push('black'); 21 console.log(obj.color) //["blue", "red", "black"] 22 23 var obj = new SubType('thanks'); 24 obj.sayName(); //thanks 25 console.log(obj.color) //["blue", "red"]
到这基本也就完了,主要的思想在于认识到prototype的动态性以及与constructor的关系,其实上边代码是可以再简化一下的,就是把这个中转函数拆开来写,那怎么改呢,其实以前是说过得:
1 function SuperType (name) { 2 this.name = name; 3 this.color = ['blue','red']; 4 }; 5 SuperType.prototype.sayName=function () { 6 console.log(this.name); 7 }; 8 function SubType(name){ 9 SuperType.call(this,name); 10 }; 11 SubType.prototype =Object.create(SuperType).prototype; 12 SubType.prototype.constructor = SubType; 13 var obj = new SubType('success'); 14 obj.sayName(); //success 15 obj.color.push('black'); 16 console.log(obj.color) //["blue", "red", "black"] 17 18 var obj = new SubType('thanks'); 19 obj.sayName(); //thanks 20 console.log(obj.color) //["blue", "red"]
归根结底,这个关键点在于这个Object.create()函数,关于这个函数在原型式继承里边说到了。
认识到函数是对象,对象的__proto__原型,函数的prototype属性,他们之间的规律后,理解起来相对来说会简单很多,感谢陪伴,写出来比想象中难多了,本来以为1天的工作量实际写起来必须把前后串联起来,考虑的也比自己单纯理解要多一点,希望能给大家带来一点想得到的东西。