引言
在JavaScript中,实现继承的主要方式是通过原型链技术。这一篇文章我们就通过介绍JavaScript中实现继承的几种方式来慢慢领会JavaScript中继承实现的点点滴滴。
原型链介绍
原型链作为JS实现继承的主要方式,其基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法。我们可以简单回顾下构造函数、原型对象和实例对象之间的关系。每一个构造函数都有一个指向原型对象的指针,当然原型对象的构造器属性也指向构造函数对象,而实例对象内部有prototype属性指向原型对象。如果我们让原型对象等于另一个引用类型的实例。那么这时候的原型对象将包含一个指向另一个原型对象的指针。当然,另一个原型对象也包含着指向另一个构造函数的指针。如果另一个原型又是另一个引用类型的实例,那么上述关系依然成立,就这样层层的推进,就构成了实例与原型的链条。这就是原型链的基本原理。
下面我们来看个实现继承的基本例子:
1 /** 2 * 实现继承的基本方式(原型链基本用法) 3 **/ 4 function SuperType() { 5 this.property = true; 6 } 7 8 SuperType.prototype.getSuperValue = function () { 9 return this.property;10 }11 12 function SubType() {13 this.subProperty = false;14 }15 16 //设置子类型的原型对象是超类的实例17 SubType.prototype = new SuperType();18 19 SubType.prototype.getSubValue = function () {20 return this.subProperty;21 }22 23 var instance = new SubType();24 alert(instance.getSuperValue()); //true
在这段代码中,我们看到了最基本的使用原型链来实现继承的方式。在这段代码中最核心的部分就是地17行代码。我们SubType的原型对象设置为SuperType的一个实例。这样存在于SuperType中的所有属性和方法现在也都存在于SubType的原型对象中。我们给SubType的原型中添加了getSubValue方法,实际就是在继承SuperType的属性和方法的基础上又添加了一个方法。
下面请看详细的关系图
通过这张图我们看到instance指向的是SubType的原型对象。而SubType原型又指向SuperType原型。通过实现原型链,本质上扩展了前面介绍的原型搜索机制。当以读取模式访问一个属性的时候,首先会在实例中进行搜索,如果搜索不到还会到实例的原型对象中进行搜索。在通过原型链继承的情况下,搜索过程就会沿着原型链继续往上搜索。例如:调用getSuperValue。1、第一步搜索instance实例对象。2、搜索SubType原型对象。3、搜索SuperType原型对象。最后在第三步才找到方法进行调用。
讲到这里,我们千万别忘记了。所有的引用类型都继承自Object对象。而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例。因此默认原型会包含一个指针指向Object.prototype。这也正是所有引用类型都会继承toString()、toValueOf()方法的根本原因。下面这张图很好的表达了这一点。
我们调用instance.toString()方法实际上调用的是Object原型对象中的toString()方法。
原型链存在的问题
原型链虽然强大,可以用它来实现继承,但是也存在一些致命的问题。其中,最主要的问题是包含引用类型值的原型。想必大家在上一篇文章中也肯定还记得在原型对象中定义引用类型值的属性会被所有的实例共享。现在使用原型链,原型实际上会变成另一个引用类型的实例。如果我们在这个实例中定义了引用类型的属性(比如:Array)。那么子类型的所有实例都会共享这个实例中的这个属性值。这实际上就回到了我们上一篇遇到的问题。所以,我们首先想到的还是借用构造函数。让实例属性和原型属性分开的方式来进行解决。
借用构造函数
在解决原型中包含引用类型值带来的问题的过程中,人们开始使用一种叫借用构造函数的方式来解决此类问题。这种方法很简单。就是在子类构造函数中调用父类构造函数。记住:函数只是在特定环境中环境中执行代码的对象,因此可以使用call()或者apply()函数来在新创建的对象上调用构造函数。请看下面的例子:
1 /** 2 * 借用构造函数 3 */ 4 function SuperType() { 5 this.color = ["red", "blue", "green"]; 6 } 7 8 function SubType() { 9 SuperType.call(this);10 }11 12 var instance1 = new SubType();13 instance1.color.push("black");14 alert(instance1.color); //"red", "blue", "green","black"15 16 var instance2 = new SubType();17 alert(instance1.color); //"red", "blue", "green"
在这段代码中,我们看第9行。SubType构造函数调用了SuperType的构造函数。通过使用call()或者apply()函数,我们在新创建的对象上调用SuperType的构造函数。这样我们会在新创建的对象上执行超类构造函数的初始化代码(this指向的是新创建的对象)。所以每一个新创建的实例都有color的副本了。
借用构造函数的问题
如果我们仅仅借用构造函数的方式,那么我们将无法避免构造函数模式带来的问题。1、方法都在构造函数中定义,根本没有办法谈函数复用的问题。2、在超类中定义的方法在子类中根本不可见,结果所有类型都只能使用构造函数模式。为了解决这个问题,我们下面介绍一种方式:组合继承。
组合继承
组合继承技术是将原型链和借用构造函数技术组合在一起使用。其背后的思想是:通过使用原型链来实现对原型属性和方法的继承,通过借用构造函数模式实现对实例属性的继承。这样,既通过在原型上定义的方法实现了函数的复用,又能保证每一个实例都有自己的属性。请看下面的例子:
1 /** 2 * 组合继承 3 **/ 4 function SuperType(name) { 5 this.name = name; 6 this.color = ["red", "blue", "black"]; 7 } 8 9 SuperType.prototype.sayName = function () {10 return this.name;11 }12 13 function SubType(name, age) {14 SuperType.call(this, name);15 this.age = age;16 }17 18 SubType.prototype = new SuperType();19 20 SubType.prototype.sayAge = function () {21 return this.age;22 }23 24 var instance1 = new SubType("Nicolas", 29);25 instance1.color.push("green");26 alert(instance1.color); //"red", "blue", "black","green"27 alert(instance1.sayName()); //Nicolas28 alert(instance1.sayAge()); //2929 30 var instance2 = new SubType("Grey", 26);31 alert(instance2.color); //"red", "blue", "black"32 alert(instance2.sayName()); //Grey33 alert(instance2.sayAge()); //26
通过以上的例子我们看到,我们在创建SubType实例的时候,调用SubType构造函数,在构造函数中通过call方法,使每一个实例上都有name、color属性的一份副本。同时设置SubType的原型为SuperType的实例,有可以进一步获取SuperType定义的共享属性和方法。实现了我们前面提到的目标。此处有一点需要注意:color属性其实对于SubType实例来说应该有两份,一份在实例中,还有一份在原型对象中,只是原型对象中的那一份被实例中的那一份覆盖了。还记得对象中属性值的搜索机制吗?
应该说,组合继承融合了原型链和构造函数方式的优点,成为JavaScript中最常用的一种继承模式。但是对于它的改进还是存在的。请看下面把。
寄生组合式继承
虽然组合继承是JavaScript中最常用的一直继承模式,但是它也存在着明显的缺点。例如:1、无论在什么情况下都需要调用两次超类的构造函数。2、子类的实例都会包含超类实例的全部实例属性,但是我们只能在调用超类构造函数的时候重写这些属性,让它覆盖原型对象中的属性值。请看下图:
其实,我们实现继承的关键无非就是一个超类原型对象的副本而已。在组合继承中我们使用超类实例作为原型对象,这样间接的关联上了超类的原型对象。但是我们无法避免子类新的原型对象中那些冗余的实例属性。如果我们定义一个自定义类型里面不存在任何实例属性,将它的原型对象设置为我们需要关联的超类的原型对象。这样不就不必存储那些不必要的实例属性了吗?看下面的代码
1 /** 2 * 组合继承(改进版) 3 **/ 4 function SuperType(name) { 5 this.name = name; 6 this.color = ["red", "blue", "black"]; 7 } 8 9 SuperType.prototype.sayName = function () {10 return this.name;11 }12 13 function SubType(name, age) {14 SuperType.call(this, name);15 this.age = age;16 }17 18 //SubType.prototype = new SuperType();19 SubType.prototype = object(SuperType.prototype);20 21 SubType.prototype.sayAge = function () {22 return this.age;23 }24 25 var instance1 = new SubType("Nicolas", 29);26 instance1.color.push("green");27 alert(instance1.color); //"red", "blue", "black","green"28 alert(instance1.sayName()); //Nicolas29 alert(instance1.sayAge()); //2930 31 var instance2 = new SubType("Grey", 26);32 alert(instance2.color); //"red", "blue", "black"33 alert(instance2.sayName()); //Grey34 alert(instance2.sayAge()); //2635 36 function object(obj) {37 function F() { }38 F.prototype = obj;39 return new F();40 }
在代码中我们我们看到,我们定义的object方法的本质就是使用一个自定义的引用类型来复制超类的原型对象。并且这个对象里面很干净,不存在任何实例属性。这初步解决了我们上面提到的属性重复存储的问题。下面咱们看看还有没有进一步的优化空间。
下面介绍一种更加科学的方法,寄生组合继承的基本模式。并且配合详细的图片给大家分析下。请看代码:
/** * 寄生组合继承基本模式 **/ function initPrototype(SubType, SuperType) { var prototype = object(SuperType.prototype); prototype.constructor = SubType; SubType.prototype = prototype; }
下面请看完整的代码例子:
1 /** 2 * 组合继承(最终版) 3 **/ 4 function SuperType(name) { 5 this.name = name; 6 this.color = ["red", "blue", "black"]; 7 } 8 9 SuperType.prototype.sayName = function () {10 return this.name;11 }12 13 function SubType(name, age) {14 SuperType.call(this, name);15 this.age = age;16 }17 18 //SubType.prototype = new SuperType();19 //SubType.prototype = object(SuperType.prototype);20 initPrototype(SubType, SuperType);21 22 SubType.prototype.sayAge = function () {23 return this.age;24 }25 26 var instance1 = new SubType("Nicolas", 29);27 instance1.color.push("green");28 alert(instance1.color); //"red", "blue", "black","green"29 alert(instance1.sayName()); //Nicolas30 alert(instance1.sayAge()); //2931 32 var instance2 = new SubType("Grey", 26);33 alert(instance2.color); //"red", "blue", "black"34 alert(instance2.sayName()); //Grey35 alert(instance2.sayAge()); //2636 37 function object(obj) {38 function F() { }39 F.prototype = obj;40 return new F();41 }42 43 /**44 * 寄生组合继承基本模式45 **/46 function initPrototype(SubType, SuperType) {47 var prototype = object(SuperType.prototype);48 prototype.constructor = SubType;49 SubType.prototype = prototype;50 }