javascript中的继承

在过去的很多时候,javascript都仅仅被用来当作一个“小工具”,比如用来处理一下异步请求、输入项校验、交互动画等等。这两年,随着html5的普及程度日趋提高和各种浏览器技术的不断推广,javascript在web开发中占据越来越重要的位置。

本文尝试向大家展示几种在javascript中常见的“类继承”手法并理清他们各自的优缺点及之间的区别,希望对大家使用javascript有所帮助。另外,本文内容均基于本人对javascript的个人理解,难免有些理解上的偏差和错误,还请各位不吝赐教。同时,阅读此文前,你除了要对javascript的简单使用有所了解,最好还会一两门面向对象的编程语言,比如java、C++、C#...


个人认为,要弄清楚javascript中的继承方式,首先要弄清楚这三个属性:prototypeconstructor__proto__

prototype: 存在于“构造函数”对象中。他本身也是一个对象,用来存放所有由该构造函数所产生的对象实例的公共属性和方法。是不是有点绕?呵呵。

constructor: 存在所有的对象中。他指向该对象的构造函数。刚刚说到,prototype属性本身也是一个对象,所以,prototype也是有construstor属性的。prototype的construstor属性指向了该构造函数本身。

__proto__: 存在于实例对象中。他指向该对象的构造函数的prototype属性。


说完了上面三个属性,接下来就说说目前常见的几种js继承的实现方式。

1、原型链

 1         function SuperType(){
 2             this.property = true;
 3         }
 4 
 5         SuperType.prototype.getSuperValue = function(){
 6             return this.property;
 7         }
 8 
 9         function SubType(){
10             this.subproperty = false;
11         }
12 
13         SubType.prototype = new SuperType();
14 
15         SubType.prototype.getSubValue = function(){
16             return this.subproperty;
17         }

在上面这段代码中,SuperType和SubType这两个函数就是我们所说的构造函数。第13行,我们将SubType的prototype属性指向了一个SuperType的实例。所以,正如刚刚我们对prototype属性所解释的那样,prototype属性“本身也是一个对象,用来存放所有由该构造函数所产生的对象实例的公共属性和方法”。所以,接下来由SubType构造函数创建的对象都共享13行中那个SuperType实例所拥有的属性和方法。当然,他们也可以再自行添加一些自己私有的属性和方法。

引用关系如下:

这种方法实现的继承虽然很简单,但是问题也很明显。刚刚我们说过,prototype这个属性指向的对象的所有方法和属性都将被这个构造函数所产生的实例引用。所以,这就会使父类中的属性呈现出“静态”特性,类似在C#类中声明的static属性。

考虑下面的代码:

 1         function SuperType(){
 2             this.nums = [0, 1];
 3         }
 4 
 5         function SubType(){
 6         }
 7 
 8         SubType.prototype = new SuperType();
 9 
10         var ins1 = new SubType();
11         console.log(ins1.nums);     //[0, 1]
12         ins1.nums.push(2);
13         
14         var ins2 = new SubType();
15         console.log(ins2.nums);     //[0, 1, 2]

可以看到,ins1实例对property属性的修改直接在ins2实例中得到了体现,这显然不是我们所希望看到的。

但这里要注意的是,只有当nums属性为引用类型值的时候,才会出现这个现象,如果nums是一个Boolean值,ins1对象对他的修改是不会影响ins2对象中该属性的值的。

这种方式实现的继承还有一个局限,就是无法给父类的构造函数传递参数。

2、借用构造函数

 为了解决上面说的两个问题,借用构造函数方式应运而生。

 1         function SuperType(n){
 2             this.nums = [0, n];
 3         }
 4 
 5         function SubType(n){
 6             SuperType.call(this, n);
 7         }
 8 
 9         var ins1 = new SubType(1);
10         console.log(ins1.nums);     //[0, 1]
11         ins1.nums.push(2);
12 
13         var ins2 = new SubType(3);
14         console.log(ins2.nums);     //[0, 3]
15         console.log(ins1.nums);     //[0, 1, 2]

由于SubType对SuperType的继承关系由第6行实现,所以,此时站在ins1和ins2的角度来看,他们其实都不知道自己是SuperType的子类的实例。

1         ins1 instanceof SuperType            //false
2         SuperType.prototype.isPrototypeOf(ins1)     //false

同时,这种实现方式还会带来另一个问题,由于方法和属性的定义都必须放在构造函数中定义,所以子类也就无法实现对父类的方法的复用了。

 1         function SuperType(){
 2             this.play = function(){}
 3         }
 4 
 5         function SubType(){
 6             SuperType.call(this);
 7         }
 8 
 9         var ins1 = new SubType();
10         var ins2 = new SubType();
11 
12         console.log(ins2.play === ins1.play);    //false

3、组合继承

总结上面的两种方法,一种(原型链)共用属性也共用方法,另一种(借用构造函数)不共用属性也不共用方法。两者都不是我们所希望的,我们想要的是共用方法但不共用属性。于是,组合继承方法解决了这个问题。

 1         function SuperType(name){
 2             this.name = name;
 3             this.foods = ['grass'];
 4         }
 5 
 6         SuperType.prototype.sayName = function(){
 7             console.log(this.name);
 8         }
 9 
10         function SubType(name, lang){
11             SuperType.call(this, name);
12 
13             this.lang = lang;
14         }
15 
16         SubType.prototype = new SuperType();
17 
18         SubType.prototype.sayLang = function(){
19             console.log(this.lang);
20         }
21 
22         var ins1 = new SubType('tiger', 'wow');
23         var ins2 = new SubType('leo', 'hoh');
 1         ins1.sayName()  //tiger
 2         ins2.sayName()  //leo
 3         ins1.sayLang()  //wow
 4         ins2.sayLang()  //hoh
 5         ins1.foods.push('pig');
 6         ins1.foods      //["grass", "pig"]
 7         ins2.foods      //["grass"]
 8         
 9         ins1.sayName === ins2.sayName   //true
10         ins1.sayLang === ins2.sayLang   //true

由上面的测试代码可以看出,ins1和ins2对于name、lang、foods这三个属性都有自己独立保存的值,第5行中ins1对foods属性的修改并不会影响到ins2中foods属性的值。

于是,通过这种方法,我们近乎完美的解决了前两个方案的问题。为什么说是“近乎”完美呢?稍后会给大家说明。

4、原型式继承

有些时候,我们只是希望创建一个和某个对象类似的对象,我们希望创建的过程非常简单,不需要写一堆构造函数、原型引用之类的东西。这时,原型式继承就派上用场了。

 1         function object(o){
 2             function F(){};
 3             F.prototype = o;
 4             return new F();
 5         }
 6 
 7         var person = {
 8             name: 'name',
 9             foods: ['rice', 'vegetables'],
10             sayHi: function(){
11                 console.log(this.name)
12             }
13         }
14 
15         var ins1 = object(person);
16         ins1.name = 'tiger';
17         ins1.foods.push('fish');
18 
19         var ins2 = object(person);
20         ins2.name = 'leo';
21 
22         console.log(ins1.sayHi());  //tiger
23         console.log(ins2.sayHi());  //leo
24         console.log(ins2.foods);    //["rice", "vegetables", "fish"]
25 
26         console.log(person.isPrototypeOf(ins2));    //true
27         console.log(ins1.sayHi === ins2.sayHi);     //true

这种方式创建的实例和我们所讲的第一种方式很类似,也会有共用引用类型属性的问题和无法向父类构造函数传参的问题。

讲到这里,估计有人就有疑问了,到底什么情况会导致共用父类属性什么情况不共用呢?看看下面这个例子。

 1         function SuperType(){
 2             this.foods = ['rice'];
 3         }
 4 
 5         function SubType1(){
 6             SuperType.call(this);
 7         }
 8         SubType1.prototype = new SuperType();
 9 
10         function SubType2(){}
11         SubType2.prototype = new SuperType();
12 
13         ins11 = new SubType1();
14         ins12 = new SubType1();
15         console.log(ins11.foods === ins12.foods);   //false
16 
17         ins21 = new SubType2();
18         ins22 = new SubType2();
19         console.log(ins21.foods === ins22.foods);   //true

SubType1与SubType2的区别在于他在自己的构造函数中显式地调用了SuperType的构造函数,也就是第6行的代码。这样的结果就是SubType1的实例,即 ins11 和 ins12,其实拥有两个foods属性!对,就是两个,一个在他们自己身上,另一个在他们的__proto__属性所指向的那个对象身上。要证明,很简单:

1         ins11.foods.push('fish');
2         console.log(ins11.foods);                   //["rice", "fish"]
3         console.log(ins11.hasOwnProperty('foods')); //true
4         delete ins11.foods;
5         console.log(ins11.foods);                   //["rice"]
6         console.log(ins11.hasOwnProperty('foods')); //false

而SubType2由于没有显式调用SuperType的构造函数,所以SubType2的实例中的foods属性其实只存在于他们的__proto__属性所指向的那个对象身上。证明也很简单:

1 console.log(ins21.hasOwnProperty('foods')); //false

现在已经弄清楚了上面的几种方案的原理和他们之间的区别,那么,是时候看看我们的终极解决方案了。

前面我们说到过,组合继承的方案已经是“近乎”完美的了,那么为什么是“近乎”而不是绝对呢?因为他也有一个问题。组合继承方案的问题在于,无论什么情况下,都会调用两次父类的构造函数:一次是在为之类指定prototype的时候,另一次是在之类构造函数内的显式调用。这导致了SubType的prototype上出现多余的、不必要的属性。

5、寄生组合式继承

 1         function inheritPrototype(subType, superType){
 2             function F(){};
 3             F.prototype = superType.prototype;
 4             var prototype = new F();
 5             prototype.constructor = subType;
 6             subType.prototype = prototype;
 7         }
 8 
 9         function SuperType(name){
10             this.name = name;
11             this.foods = ['rice'];
12         }
13         SuperType.prototype.sayName = function(){
14             console.log(this.name);
15         }
16 
17         function SubType(name, age){
18             SuperType.call(this, name);
19             this.age = age;
20         }
21 
22         inheritPrototype(SubType, SuperType);
23 
24         SubType.prototype.sayAge = function(){
25             console.log(this.age);
26         }
27 
28         var ins1 = new SubType('tiger', 27);
29         var ins2 = new SubType('leo', 28);
30         ins1.sayName();             //tiger
31         ins1.sayAge();              //27
32         ins2.sayName();             //leo
33         ins2.sayAge();              //28
34 
35         ins1.foods.push('fish');
36         console.log(ins1.foods);    //["rice", "fish"]
37         console.log(ins2.foods);    //["rice"] 
38 
39         delete ins1.name;
40         console.log(ins1.name);     //undefined

如上所示,寄生组合式继承完美解决了之前几种方案的所有问题。他是如何实现的?看这幅图:

 

从这幅图可以看出,ins1和ins2的constructor均指向了SubType函数,而SubType函数中显式调用了SuperType的构造函数,所以,ins1和ins2两个对象本身就会有了SuperType中定义的name和foods属性,而且他们的这两个属性是定义在对象自身上的,相互不会干扰。

同时,ins1和ins2的__proto__属性均指向了inheritPrototype函数中定义的prototype这个对象实例,而prototype这个对象实例的__proto__属性指向了SuperType的prototype对象。所以ins1和ins2两个对象实例均从SuperType的prototype属性继承来了sayName方法。


至此,javascript中常见的几种继承机制就讲完了,不知道大家看到这里的时候有没有真正弄明白这里面的原理。其实我个人觉得这些原理还是比较绕的(我花了超过两天时间才完全理清这些关系这种事情我会随便告诉你们么,哼~~),但我认为如果你在看得有点糊涂的时候回头去想想我文章最前面说的那三个属性的含义,也许思路就又能变清晰一些了。在学习继承这个知识点的时候我也走了不少弯路,希望这篇文章能给大家一点点的帮助或启发。

 参考资料:

《JavaScript高级程序设计:第2版》

《JavaScript语言精粹》

Web程序员应该知道的Javascript prototype原理

Javascript继承机制的设计思想

如需转载,请注明转自:http://www.cnblogs.com/silenttiger/p/3182174.html

欢迎关注我的微信公众号:老虎的小窝
微信公众号 老虎的小窝

原文地址:https://www.cnblogs.com/silenttiger/p/3182174.html