JavaScript的原型和原型链的前世今生(二)
3.1、原型对象
在上一篇文章中我们讲到的prototype
属性,这个属性指向一个对象,而这个对象的用途是包含可以由特定类型
的所有实例
共享的属性和方法,在标准中我们称此对象为原型对象。原型对象会在创建一个新函数的时候根据一组特定的规则来生成。
既然有prototype
这个属性,为什么上一篇文章中浏览器所截图的却都是__proto__
?**原因是这二者压根就不是同一个东西!!**根据ECMA-262第五版中的介绍,实例内部的指针明确称为[[Prototype]],虽然没有标准的方式访问该指针,但Firefox、Safari、Chrome在每个对象上都支持一个属性__proto__
,所以你所见到的__proto__
其实是浏览器自己实现的一个访问的接口,而不是标准设定的,但是等价于标准定义的[[Prototype]]
。但prototype
属性却是构造函数特有的,它永远指向构造函数的原型对象。因此这两篇文章中所有示意图标记的[[Prototype]]
和prototype
是有其固定的含义的,切不可混淆了。
说了这么多,问个问题:__proto__
是谁与谁的连接?自己可以好好琢磨哈。
虽然无法访问到[[Prototype]],但是可以使用isPrototypeof()方法来确定对象之间是否存在关系,也可以使用getPrototypeof()来获取[[Prototype]]的值。
既然原型对象的属性在实例化的对象中也可以访问到,那么有什么办法来判断访问的属性是在实例化的对象中还是在原型对象中呢?答案便是isOwnProperty()。
for-in循环将会遍历所有能够通过对象访问、可枚举的属性,无论该属性位于实例中还是原型中。在《JavaScript高级程序设计(第三版)》的6.2节有这么一段话,个人觉得有点多余:
屏蔽了原型中不可枚举属性(即将[[Enumerble]]标记为false的属性)的实例属性也会在for-in循环中返回。
按照作者的理解是如果你在实例属性中定义了一个与原型中同样的名字的属性,并且在原型中该属性是不可枚举的,那么for-in依然会返回该属性。这个其实是很明显的命题,因为for-in在实例中找到该属性,并且该属性是可枚举的(除非你手动设置其为不可枚举)。
一个个枚举所有可枚举的属性比较麻烦,好在ES5提供了
Object.keys()
方法来获取所有可枚举的属性。如果想获取所有实例属性,可以使用Object.getOwnPropertyNames()
。
之前说过原型模式也是有缺点,其最大的缺点便是其共享的特性,随便修改原型对象中的任何一个属性,都会影响到它所实例化的所有对象,这样造成了不能出现“求同存异”的现象。因此我们会更多地使用下面的一种方法。
3.2、组合使用构造函数模式和原型模式
组合使用当然是将所有共享的属性放在原型对象中,所有独特的属性放在构造函数中,这样的话可以实现真正的“求同存异”了。比如:
1function Animal(){ 2 this.name = name; 3 this.type = type; 4 this.say = function(){ 5 console.log('I am a ' + this.type); 6 } 7} 8 9Animal.prototype = { 10 constructor: Animal; 11 feetCount: 0; 12 run: function(){ 13 console.log('I can run'); 14 } 15} 16 17var dog = new Animal('WangWang', 'dog');
注意:
为什么这里的Animal.prototype要重新赋值constructor?
童鞋们结合上一篇文章可以思考一下!!
那么我们是否可以考虑将上面的代码再优化一下,减少代码量?这时可以使用动态原型模式
:
1function Animal(){ 2 this.name = name; 3 this.type = type; 4 this.say = function(){ 5 console.log('I am a ' + this.type); 6 } 7 8 if (typeof this.run != 'function'){ 9 Animal.prototype.feetCount = 0; 10 Animal.prototype.run = function(){ 11 console.log('I can run'); 12 } 13 } 14} 15 16var dog = new Animal('WangWang', 'dog');
注意:
为什么这里初始化原型的时候不能使用上面例子中的对象字面量的形式?
童鞋们也可以自己思考一下!!
在《JavaScript高级程序设计(第三版)》还介绍了两种模式来创建对象:
寄生(parasitic)构造函数模式
和稳妥(Durable)构造函数模式
,细节可以参考书本。
4、原型链
想必讲到这里,你应该已经能够猜到原型链的实现原理了。ES5使用原型链来作为实现继承
的主要方法(由于函数没有签名。在ES5中无法实现接口继承
)。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。我们完善一下上一节的图片3:
可以看出,通过[[prototype]]
属性将实例、原型对象、原型对象的原型对象串联起来了,这个串联便是原型链。
同样原型链也是存在两个问题:
- 原型中包含引用类型值的问题(也就是刚才3.1节说的问题)
- 在创建子类型实例时,不能向超类型的构造函数中传递参数
因此常用的解决方案有下面几个:
4.1、借用构造函数
借用构造函数(constructor stealing)(有时候也叫作伪造对象或经典继承),实现原理很简单,那就是在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象, 因此通过使用 apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数。比如:
1function Species(){ 2 this.colors= ['red', 'green']; 3} 4 5function Animal(type, name){ 6 Species.call(this); 7 this.type = type; 8 this.name = name; 9} 10 11var dog = new Animal('dog', 'WangWang'); 12dog.colors.push('black'); 13 14var cat = new Animal('cat', 'MiMi'); 15cat.colors.push('yellow');
因为使用call函数的方法,让实例化Species超类的时候this指针指向了实例化的子类,相当于colors成了子类Animal的属性,因此每个实例操作的colors都是自己的私有属性。如下图:
因为使用call方法,我们还可以传递参数给超类:
1function Species(feet){ 2 this.colors= ['red', 'green']; 3 this.feet = feet 4} 5 6function Animal(type, name, feet){ 7 Species.call(this, feet); 8 this.type = type; 9 this.name = name; 10}
这种方法导致的问题也就是构造函数的通病--那就是方法无法复用,每个实例都有自己一个实例的方法,所以一般采用上面的方式来使用。
4.2、组合继承
组合继承(combination inheritance)有时候也叫作伪经典继承。该方案结合了借用构造函数(参考4.2.1小节)和原型链的。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。看下面的例子:
1function Species(name, type){ 2 this.name = name; 3 this.type = type; 4} 5 6Species.prototype.run = function(){ 7 console.log('I can run !'); 8} 9 10function Animal(name, type, age){ 11 Species.call(this, name, type); 12 13 this.age = 0; 14} 15 16Animal.prototype = new Species; 17Animal.prototype.constructor = Animal; // 为什么? 18Animal.prototype.reportAge = function(){ 19 console.log('my age is ' + this.age); 20} 21 22var dog = new Animal('WangWang', 'dog', 11); 23dog.run(); 24dog.reportAge();
上面代码中为什么要重新复制Animal原型的构造函数呢?我们先举个例子:
1function Person(){} 2Person.prototype = { 3 name : "Nicholas", 4 age : 29, 5 job: "Software Engineer", 6 sayName : function () { 7 alert(this.name); 8 } 9};
我们知道每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。所以刚开始创建Person函数的时候,默认Person.prototype.constructor == Person
,但是因为在下一行代码中重新给Person的原型对象换了一个新的对象,而新的对象的默认constructor属性是指向Object构造函数的,所以导致你的Person的原型对象的构造函数就不再指向Person。如果 constructor 的值真的很重要,那么需要你重新纠正这个指向,于是就有刚才的那行代码Animal.prototype.constructor = Animal;
。
该段代码的原型链图如下:
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继 承模式。而且, instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象
4.3、其他方案
除了组合继承,还有原型式继承、寄生式继承、寄生组合式继承等三种方法,后面的这三种方法用到的地方不多,所以不做介绍,细节可以参考《JavaScript高级程序设计(第三版)》。
5、总结
通过这篇文章,从最初最简单的对象创建到后面的构造函数模式、原型模式创建对象,可以看出这门语言的强大生机,在不断地优化中变得越来越有意思。我们从这些演变中也掌握了对象的创建方法,顺便学习了原型以及原型链那些比较晦涩的概念。深究一个概念往往便是深究其演变的历史,所以要知未来,历史不可忘也!!
6、参考
[1] 《JavaScript高级程序设计(第三版)》第6章
[2] MDN
[3] ES5标准
公众号关注一波~
![微信公众号](https://blogimage.5udou.cn/homePage/%E5%85%AC%E4%BC%97%E5%8F%B7-%E7%99%BD%E8%89%B2%E7%89%88.png?x-oss-process=style/addWaterMarkBottom)
网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 JavaScript的原型和原型链的前世今生(二) 的内容有疑问,请在下面的评论系统中留言,谢谢。