本文继续讲解JavaScript的面向对象程序设计。继承是面向对象语言中的一个基本概念,面向对象语言支持两种继承实现方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。但是在JavaScript中函数时没有签名的,所以无法实现接口继承。JavaScript支持实现继承,而且其实现继承主要是通过原型链继承的。
- 原型链
JavaScript中有原型链的概念,并将原型链作为实现继承的主要方法。基本实现思想是让一个函数的原型继承另外一个函数的原型的属性和方法。每一个函数都有一个原型对象,原型对象包含一个指向构造函数的指针,实例包含一个指向原型对象的指针。原型链的概念就是,一个原型对象指向另一个函数的原型,同样另一个函数的原型又指向其他函数的原型,层层递进,就构成了原型链。
1 function SuperType(){ 2 3 } 4 function SubType(){ 5 6 } 7 SubType.prototype=new SuperType(); 8 function ExtendType(){ 9 10 } 11 ExtendType.prototype = new SubType();
上面的代码展示了原型链的概念,ExtendType的原型指向了SubType实例,SubType的原型指向了SuperType实例,SuperType的原型指向了Object的原型。这样就形成了一个原型链。原型链本质上是扩展了前面介绍的原型搜素机制。当访问实例的属性或者方法时,首先搜索实例的属性或者方法,再搜索实例的原型。通过原型链,可以一直向上搜索,直至搜索到Object的原型。
前面也简单地介绍了确认原型的方法。
1 var extendtype = new ExtendType(); 2 console.log(extendtype instanceof Object); 3 console.log(extendtype instanceof SubType); 4 console.log(extendtype instanceof SuperType);
上面的代码中2、3、4行都输出true,这说明extendtype中能找到函数的原型。也可以通过另外一种方法实现原型的判断,就是通过原型的isPrototypeOf方法。
1 console.log(Object.prototype.isPrototypeOf(extendtype)); 2 console.log(SubType.prototype.isPrototypeOf(extendtype)); 3 console.log(SuperType.prototype.isPrototypeOf(extendtype));
上面的代码通过isPrototypeOf方法来判断实例的类型。同样,输出都是true。
通过原型链在JavaScript中实现的继承依然存在一定的问题。原型链会将实例中的所有属性都共享,但是我们在构造函数中定义属性,二不在原型中定义属性就是为了不共享属性
1 function Super(){ 2 this.colors=["green"]; 3 } 4 function Sub(){ 5 6 } 7 Sub.prototype= new Super(); 8 var sub = new Sub(); 9 sub.colors.push("red"); 10 console.log(sub.colors.toString());//green,red 11 var sub2 = new Sub(); 12 console.log(sub2.colors.toString());//green.red
上面的代码定义两个对象super和sub,sub的原型继承了super的实例。我们创建了sub的两个实例,对其中的一个实例colors的属性添加了一个元素,但是我们发现两个实例的属性都改变了。因为该两个实例的colors属性都指向super中的属性。
- 借用构造函数
借用构造函数的思想就是在子类型的构造函数中调用父类的构造函数,可以通过apply或者call调用父类构造函数。
1 function Super(){ 2 this.colors=["green"]; 3 } 4 function Sub(){ 5 Super.call(this); 6 } 7 var sub = new Sub(); 8 sub.colors.push("red"); 9 console.log(sub.colors.toString());//green,red 10 var sub2 = new Sub(); 11 console.log(sub2.colors.toString());//green
上面的代码在子类的函数中调用了父类的构造函数,通过call。同时,我们实例化了两个子类对象,发现sub的操作并没有影响sub2的结果。每次实例化都会调用父类的构造函数,这样每个sub都有自己的colors属性。
同时,通过借用构造函数,我们还能传递参数。
1 function Super(name){ 2 this.colors=["green"]; 3 this.name=name; 4 } 5 function Sub(name){ 6 Super.call(this,name); 7 } 8 var sub = new Sub("hehe"); 9 sub.colors.push("red"); 10 console.log(sub.name);//hehe 11 var sub2 = new Sub("haha"); 12 console.log(sub2.name);//haha
上面的代码,我们通过构造函数传递了参数,并通过call方法传递参数给父类的构造函数。借用构造函数和构造函数模式创建对象拥有同样的问题,方法和属性都在构造函数中定义,因为函数无法复用。也无法判断函数的类型。
- 组合式继承
组合继承是指将原型链和构造函数的技术组合在一起。它的思路是通过原型链实现属性和方法的继续,通过借用构造函数模式实现实例属性的继承。这样在原型链上实现方法,保证函数的复用,同时又保证每个实例有自己的属性。
1 function Super(name){ 2 this.name=name; 3 } 4 Super.prototype.getName=function(){ 5 return this.name; 6 } 7 function Sub(name,age){ 8 Super.call(this,name); 9 this.age=age; 10 } 11 Sub.prototype= new Super(); 12 var sub = new Sub("haha",18); 13 console.log(sub.getName());//haha 14 console.log(sub.age);//18 15 var sub2 = new Sub("hehe",19); 16 console.log(sub2.getName());//hehe 17 console.log(sub2.age);//19
上面的代码中Super定义了一个属性name和一个原型方法getName,sub定义了一个实例属性age。sub继承了super的实例。也就是sub用super的原型方法,同时能够调用super的实例属性。在后面定义了两个sub实例,他们是不同的实例,拥有不同的实例属性,但是他们共享了原型方法。
组合继承避免了原型链和构造函数的缺陷,是一种常用的继承实现方法。
- 原型式继承
克罗克福德提出了原型式继承的方法。他的方法是借助原型基于已有的对象创建新的对象,同时还不必因此创建自定义类型。
1 function create(o){ 2 function F(){}; 3 F.prototype=o; 4 return new F(); 5 }
在create函数内部,先创建了零时行的函数F,并将F的原型指向参数o,参数o是另一个对象的原型,最后返回F的实例,并且该实例继承了传递进行来的对象的原型。
1 function create(o){ 2 function F(){}; 3 F.prototype=o; 4 return new F(); 5 } 6 var Person={ 7 "name":"haha", 8 getName:function(){ 9 return this.name; 10 } 11 } 12 var oneperson=create(Person.prototype); 13 oneperson.name="hehe"; 14 console.log(oneperson.name);//hehe 15 var twoperson = create(Person.prototype); 16 twoperson.name="jack"; 17 console.log(twoperson.name);//jack
上面的代码,基于create函数创建了两个对象,这两个对象继承了Person。这就意味着Person中的属性和方法,oneperson中同样拥有。ECMAScript5中定义了新的方法Object.create()方法,该方法有两个参数,一个参数是一个对象原型,另一个参数是需要生成的新属性。与上面的方法类似。
- 寄生式继承
寄生式继承与寄生式函数的工厂模式类似,也是创建一个用于封装继承过程的函数。在函数内部以一定的方式增强对象,最后返回对象。
1 function create(o){ 2 function F(){}; 3 F.prototype=o; 4 return new F(); 5 } 6 function createPerson(o){ 7 var f=create(o); 8 o.sayHi=function(){ 9 console.log("hi"); 10 } 11 return o; 12 } 13 var Person={ 14 name:"haa", 15 age:"8" 16 } 17 var one=createPerson(Person);
上面的one不仅继承了person的属性,同时还拥有增强属性sayHi。
- 寄生组合式继承
前面说过最常用的继承方式是组合式继承,但是组合继承,不论什么情况,都要父类构造函数两次。第一次是在子类继承父类的实例时候,第二次是子类实例化过程中。
寄生组合式继承,通过借用构造函数来继承属性,通过原型链来继承方法。使用寄生式继承来继承父类的原型,不必通过实例化来继承父类,这样减少了调用父类构造函数的次数,只用调用一次。
1 function create(o){ 2 function F(){}; 3 F.prototype=o; 4 return new F(); 5 } 6 function SuperType(name){ 7 this.name=name; 8 } 9 function Sub(name,age){ 10 SuperType.call(this,name); 11 this.age=age; 12 } 13 Sub.prototype=create(SuperType.prototype); 14 Sub.prototype.getName=function(){ 15 return this.name; 16 } 17 var sub = new Sub("haha",19); 18 console.log(sub.getName());// 19 var sub2 = new Sub("hehe",18); 20 console.log(sub2.getName());//hehe