写在前面的话:这篇博客不适合对面向对象一无所知的人,如果你连_proto_、prototype...都不是很了解的话,建议还是先去了解一下JavaScript面向对象的基础知识,毕竟胖子不是一口吃成的。博文有点长,如果能仔细看懂每一句话(毕竟都是《高程3》的原话),收获不容小觑。有关面向对象的基础知识,请参见:JS的从理解对象到创建对象.
我们都知道面向对象语言的三大特征:继承、封装、多态,但JavaScript不是真正的面向对象,它只是基于面向对象,所以会有自己独特的地方。这里就说说JavaScript的继承是如何实现的。
学习过Java和c++的都知道,它们的继承通过类实现,但JavaScript没有类这个概念,那它通过什么机制实现继承呢? 答案是: 原型链! 其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
这篇博客主要是关于《高程3》—— 6.3 继承 的总结,建议先阅读阮一峰大神的js继承三部曲,然后再回头看体会更深:
JavaScript面向对象编程(一):封装
JavaScript面向对象编程(二):构造函数的继承
JavaScript面向对象编程(二):非构造函数的继承
下面这个是关于Function和Object创建实例之间的关系,不妨先了解一下它们之间复杂的关系:
图 1
实现继承之前,先看一个基于原型链继承的链图,对继承有个具体化的概念: (这个是核心继承部分)
图 3
-------------------------------有了上面的思路,下面来看js中6种经典的实现继承方法-----------------------------------------
导读提示:方法1-3为一个体系,方法4-6为另一个体系.
1、原型链继承
简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针([[Prototype]])。 那么,假如我们让原型对象(Prototype)等于另一个类型的实例,结果会是怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型也包含着一个指向另一个构造函数的指针,假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层推进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念,可能有些绕口,下面结合代码理解。
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 //继承SuperType 14 SubType.prototype = new SuperType(); 15 16 SubType.prototype.getSubValue = function() { 17 return this.subproperty; 18 } 19 20 var instance =new SubType(); 21 console.log(instance.getSuperValue()); // true
直观的模型图请参见图 2,同时,所有继承都离不开Object() 这个终极Boss,因此,完整的原型链的原点就是Object对象,参见图 3.
1.1 确定原型和实例的关系:
实例沿着原型链向上查询,只要是自己继承的,都被认作自己的构造函数,测试如下
1 var instance =new SubType(); 2 console.log(instance instanceof Object); //true 3 console.log(instance instanceof SuperType); //true 4 console.log(instance instanceof SubType); //true 5 6 var instance1 =new SuperType(); 7 console.log(instance1 instanceof Object); //true 8 console.log(instance1 instanceof SuperType); //true 9 console.log(instance1 instanceof SubType); //false
1.2 谨慎定义方法:
子类型又时需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎么样,给原型添加方法的代码一定要放在替换原型的语句之后。看正确代码:
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 //继承SuperType 14 SubType.prototype = new SuperType(); 15 16 //添加新方法 ###必须放在上一句继承SuperType之后,否则调用这个方法时会报错--没定义 17 SubType.prototype.getSubValue = function() { 18 return this.subproperty; 19 }; 20 21 //重写超类中的方法 ###必须放在上一句继承SuperType之后,否则调用这个方法时重写失效 22 SubType.prototype.getSuperValue = function() { 23 return false; 24 } 25 26 var instance = new SubType(); 27 console.log(instance.getSuperValue()); //false 28 console.log(instance.getSubValue()); //false
!!!注意:通过原型链实现继承时,不能使用对象字面量形式创建原型方法。因为那样会重写原型链,举个栗子:
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 //继承SuperType 14 SubType.prototype = new SuperType(); 15 16 //使用字面量添加新方法,会导致上一行代码无效 17 SubType.prototype ={ 18 getSubValue : function() { 19 return this.subproperty; 20 }, 21 22 someOtherMethod : funtion (){ 23 return false; 24 } 25 }; 26 27 var instance = new SubType(); 28 console.log(instance.getSuperValue()); // error!
以上代码展示了刚刚把SuperType的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个Object的实例,而非SuperType的实例,因此我们设想中的原型链已经被切断——SubType和SuperType之间已经没有关系了,即继承语句SubType.prototype = new SuperType() 失效
1.3 原型链继承的问题
第一个问题来自包含引用类型值的原型,因为它有这么一个特性:包含引用类型值的原型属性会被所有实例共享(修改),而在构造函数中的基本类型和引用类型属性均不可改变(const附体);这也是为什么要在构造函数中,而不是原型对象中定义属性的原因。这里通过原型来实现继承,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了,进而会被所有子类实例共享(修改)。补充一句:虽然可以通过实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
1 function SuperType() { 2 this.colors = ["red","blue","green"]; //构造函数属性(实例属性),会被实例共享,但不会被修改 3 } 4 5 function SubType() { 6 } 7 8 //继承了SuperType 9 SubType.prototype = new SuperType(); /*原先的实例属性也就顺理成章地变成了现在的原型属性*/ 10 11 var instance1 = new SubType();
/* instance1.colors = ["red","blue","green","black"]; 这种方式是给instance1新添加的属性,覆盖了原型colors,而不是修改了原型colors. */ 12 instance1.colors.push("black"); 13 console.log(instance1.colors); //"red,blue,green,black" 14 15 var instance2 = new SubType(); 16 console.log(instance2.colors); //"red,blue,green,black" Super中的实例属性也变成可以被改写的,不理想
第二个问题就是:在创建子类型的实例时,不能向超类的构造函数中传递传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链继承。
2、借用构造函数
为了解决原型链继承带来的问题,一种新的继承应运而生——借用构造函数,其基本思想很简单:在子类型构造函数的内部调用超类型。别忘了,函数只不过是在特定环境中执行的对象,因此通过使用apply()、call()方法也可以在(将来)新创建的对象上执行构造函数,代码如下:
1 function SuperType() { 2 this.colors = ["red","blue","green"]; 3 } 4 5 function SubType() { 6 //继承了SuperType --重新创建SuperType构造函数属性的副本 7 SuperType.call(this); 8 } 9 10 var instance1 = newe SubType(); 11 instance1.colors.push("black"); 12 console.log(instance1.colors); //"red,blue,green,black" 13 14 var instances2 = new SubType(); 15 console.log(instance2.colors); //"red,blue,green" --完美实现了继承构造函数属性
代码中加粗那一行“借调”了超类的构造函数。通过使用call()方法(或apply()方法),我们实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新的SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性副本了。
2.1 传递参数
相对于原型链继承而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传参。看下面这个例子
1 function SuperType() { 2 this.name = name; 3 } 4 5 function SubType() { 6 //继承SuperType,同时还传递了参数 --重新创建SuperType构造函数属性的副本 7 SuperType.call(this,"Nicholas"); 8 9 //实例属性 10 this.age = 23; 11 } 12 13 var instance = new SubType(); 14 console.log(instance.name); // "Nicholas" 15 console.log(instance.age); // 23
注意:为了保证子类构造函数属性不会被超类重写,可在调用超类构造函数后,再添加应该在子类中定义的属性。
2.2 借用构造函数问题
如果仅仅是借用构造函数,那么也无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此,函数的复用就无从谈起。而且,在超类的原型中定义的方法,对子类而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这个问题,借用构造函数的技术也是极少单独使用的。
3、组合继承
组合继承,指的是将原型链继承和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承模式。思路是:利用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性。
1 function SuperType(name) { 2 this.name = name; 3 this.colors = ["red","blue","green"]; 4 } 5 6 SuperType.prototype.sayName = function() { 7 console.log(this.name); 8 }; 9 10 function SubType(name,age){ 11 //继承属性 --重新创建SuperType构造函数属性的副本 12 SuperType.call(this,name); 13 14 this.age = age; 15 } 16 17 //继承方法 18 SubType.prototype = new SuperType(); 19 SubType.prototype.constructor = SubType; 20 SubType.prototype.sayAge = function() { 21 console.log(this.age); 22 }; 23 24 var instance1 =new SubType("Nicholas",29); 25 instance1.colors.push("black"); 26 console.log(instance1.colors); // "red,blue,green,black" 27 instance1.sayName(); // "Nicholas" 28 instance1.sayAge(); // 29 29 30 var instance2 = new SubType("Greg",22); 31 console.log(instance2.colors); // "red,blue,green" 32 instance2.sayName(); // "Greg" 33 instance2.sayAge(); // 22
组合继承避免了原型链和借用构造函数的缺陷,融合它们的优点,成为JavaScript中最常用的继承模式。
4、原型式继承
这是另一种继承,没有严格意义上的构造函数。思路是:借助原型可以基于已有的对象创建新对象,同时还不必要创建自定义类型。
1 function object(o) { 2 function F() {} 3 F.prototype = o; 4 return new F(); 5 }
在object()函数内部,先创建一个临时的构造函数,然后将传入的对象作为构造函数的原型,最后返回这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。例子如下:
1 var person = { 2 name : "Nicholas", 3 friend : ["Shelby","Court","Van"] 4 }; 5 6 var anotherPerson = object(person); 7 anotherPerson.name = "Greg"; 8 anotherPerson.friends.push("Rob"); 9 10 var yetAnotherPerson = object(person); 11 yetAnotherPerson.name = "Linda"; 12 yetAnotherPerson.friends.push("Barbie"); 13 14 console.log(person.friends); // "Shelby,Court,Van,Greg,Barbie"
ECMAScript5通过Object.create()方法规范了原型式继承。这个方法接受俩个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。支持Object.create()方法的浏览器有IE9+、Firefox4+、Safari5+、Opera12+和Chrome。
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样,一变全变!
5、寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,与寄生式构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。以下代码示范了寄生式模式
1 function createAnother(original) { 2 var clone = object(original); //通过调用函数创建一个新对象 3 clone.sayHi = function() { //以某种方式来增强这个对象 4 console.log("hi"); 5 }; 6 return clone; // 返回这个对象 7 } 8 9 var person = { 10 name : "Nicholas", 11 friend : ["Shelby","Court","Van"] 12 }; 13 14 var anotherPerson = createAnother(person); //继承person的属性和方法,同时有自己的属性和方法 15 anotherPerson.sayHi(); //hi
使用寄生式继承来为对象添加函数,会由于不能做到函数的复用而降低效率;这一点和构造函数继承模式类似。
6、寄生组合式继承
前面说过,组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用俩次超类型构造函数SuperType():一次是在创建子类型原型的时候( SuperType.call(this,name); ),另一个是在子类型构造函数内部( SubType.prototype = new SuperType(); )。
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。寄生组合式继承的基本思路:不必为了指定子类型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下:
1 function inheritPrototype(subtype, supertype){ 2 var prototype = object(superType.prototype); // 创建对象 3 prototype.constructor = subType; // 增强对象 4 subType.prototype = prototype; // 指定对象 5 }
这个示例中的inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接收2个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为了创建的副本添加constructor属性,从而弥补因重写而失去的默认的constructor属性,保证还能使用instanceof和isPrototypeOf()。最后一步,将新建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句( SubType.prototype = new SuperType(); )。
至此,JavaScript继承的几种常用方法到此结束,重点难点还是要弄清构造函数、原型、实例之间的关系,什么情况下原型会被修改?怎样继承才能使原型不被修改?原型是怎样被实例继承的?构造函数属性又是怎么被实例继承的(这个需要去了解 new都做了啥 这个知识点)?
最后,若发现错误之处,请留言告之,不胜感激!_^_
参考书籍:《高程》6.3