曾经,我写过下面一段代码,我满心欢喜觉得获得了JS面向对象编程和原型继承的真谛。程序员
var pets = { sound: '', makeSound: function() { console.log(this.sound); } } var cat = { sound: 'miao' }; cat.prototype = pets; cat.makeSound();复制代码
而后,我将这段代码复制粘贴到浏览器的console调试工具下运行,居然报出一个错误。我不得不认可,原来我根本就不懂JS的面向对象编程。编程
个人目的是,让cat继承pets的makeSound方法,虽然cat没有makeSound方法,可是它能够沿着原型链查找到pets的makeSound方法。可是,很明显,这段代码有错误,没法达到个人预期。我认识到,我根本就没有弄懂过prototype
和__proto__
属性的关系和做用。浏览器
若是你也不知道上面的代码确切的错在哪里,那你也须要补上这一课。若是你知道上面的代码错在哪里,但不知道为何是这样的安排,这篇文章也能让你有收获,如标题所言,知其然,亦知其因此然。markdown
为了讲清楚这个问题,咱们先抛开上面的错误,从构造一个简单对象开始谈起。函数
咱们将从一个简单的工厂函数开始:工具
var Pets = function(sound) { var obj = { sound: sound }; obj.makeSound = function() { console.log(obj.sound); } obj.bite = function() { console.log('bite'); } return obj; } var dog = Pets('wang'); dog.makeSound(); // wang复制代码
上面定义了一个宠物制造工厂,它生成了一个拥有sound属性的对象,并将接收的参数赋值sound属性。而后在该对象上添加了两个方法,最后将这个对象返回。有了这个函数,咱们就能够制造出各类各样的宠物了。(为了先后一致,请忽略函数首字母大写的问题)性能
然而,上面的工厂函数有一个缺点。若是咱们想给这个工厂函数添加更多的方法,或者删除多余的方法,那么咱们不得不改动这个函数自己的代码。当方法变得愈来愈多的时候,这个函数就变得难以维护。因此,咱们进行以下优化:优化
var Pets = function(sound) { var obj = { sound: sound }; extend(obj, Pets.methods); // 注意,这里的extend函数是没有实现的。 return obj; } Pets.methods = { makeSound: function() { console.log(this.sound); }, bite: function() { console.log('bite'); } } var dog = Pets('wang'); dog.makeSound() // wang复制代码
能够看到,咱们给Pets函数添加了一个methods属性,用来统一保存和维护该工厂函数的方法。当使用该函数生成obj对象时,经过一个extend函数将Pets.methods中的方法通通复制到obj中。这时,代码本质上没有改变什么,只是经过形式上的改变,使得代码更容易维护。若是咱们想给Pets工厂函数添加新的方法,能够经过下面的方式实现,而没必要修改函数:this
Pets.methods.scratch = function() {/*...*/}复制代码
上面的代码,每次调用工厂函数生成的新对象,都有一份对Pets.methods中方法的彻底复制。这种建立对象的方式是低效的,既然Pets.methods中的方法是全部由工厂函数建立的对象都拥有的,咱们其实并不但愿每一个对象都保留一份复制,而是但愿经过某种方式,让全部的对象共享方法,因此就有了继承的概念。在JS中,Object.create
函数能够实现继承的目的,咱们将代码改写以下:spa
var Pets = function(sound) { var obj = Object.create(Pets.methods); obj.sound = sound; return obj; } Pets.methods = { makeSound: function() { console.log(this.sound); }, bite: function() { console.log('bite'); } } var dog = Pets('wang'); dog.makeSound(); // wang dog.bite(); // bite复制代码
Object.create
构建了一个继承关系,即obj继承了Pets.methods的方法。obj内部有一个[[Prototype]]
指针,指向了Pets.methods,Pets.methods也就成了该对象的原型对象。[[Prototype]]
指针是一个内部属性, 脚本中没有标准的方式访问它,可是在Chrome、 Safari、Firefox中支持一个属性__proto__
,而在其余浏览器实现中,这个属性都是彻底不可见的。在Chrome的调试窗口打印dog:
能够看到,dog并不拥有makeSound方法,但仍然可使用该方法,由于它能够沿着__proto__
指针指明的方向继续查找makeSound方法,一旦找到同名方法就返回该方法。(任何对象都继承自Object对象,因此方法查找的终点在Object处,假如查找到达Object对象且Object对象也没有该方法,则返回undefined)
上面的改进,经过继承,将对象的公用方法委托给原型对象,每次建立新的对象时,就免去了属性的复制,提升了代码的性能和可维护性。下面,咱们对代码进行一点小改动:
var Pets = function(sound) { var obj = Object.create(Pets.prototype); obj.sound = sound; return obj; } Pets.prototype.makeSound = function() { console.log(this.sound); } Pets.prototype.bite = function() { console.log('bite'); } var dog = Pets('wang'); dog.makeSound(); // wang dog.bite(); // bite复制代码
咱们把做为原型对象的Pets.methods换了一个名称,叫作Pets.prototype
。是否是以为哪里不对?怎么能这么随意的替换呢?prototype
但是JS语言中很特殊的一个属性,有着某种很特别的功能,怎么可能和这里的methods同样呢?没错,这么替换,而不是一开始就使用prototype
,就是想说明,其实,prototype
属性并无什么神秘的地方,它的做用和这里的methods几乎是同样的。
上面的这种建立对象,并将对象方法委托到原型对象的方式,在JS编程中是如此的常见,因此语言自己提供了一个方法,将重复的部分自动处理,程序员只须要关注每一个对象不相同的部分,这个方法就是,构造函数:
var Pets = function(sound) { this.sound = sound; } Pets.prototype.makeSound = function() { console.log(this.sound); } Pets.prototype.bite = function() { console.log('bite'); } var dog = new Pets('wang'); dog.makeSound(); // wang dog.bite(); // bite var cat = new Pets('miao'); cat.makeSound(); // miao cat.bite(); // bite复制代码
构造函数的new
操做,自动处理了继承和返回操做。能够这么理解new
的主要做用:
var Pets = function(sound) { /* this = Object.create(Pets.prototype); */ this.sound = sound; /* return this; */ }复制代码
就好像在执行new操做的时候,语言自动处理了注释部分的代码,只须要咱们关注将要建立的对象的特殊部分便可。(固然,上面的代码去掉注释是没法运行的,由于this是只读的,不能赋值,浏览器运行会报错。但原理是正确的。)
prototype
则为构造函数的一个属性,也是由构造函数所建立对象的原型对象。若是必定要说prototype和前面例子中的methods有什么不一样,那就是,prototype
有一个默认属性constructor
,该属性指向构造函数自己。
console.log(Pets.prototype.constructor === Pets) // true复制代码
顺便,你认为下面的表达式应该打印什么?
console.log(dog.constructor)复制代码
应该是Pets,dog自身没有constructor
属性,因此沿着原型链向上查找,找到Pets.prototype,而Pets.prototype是有这个属性的,返回这个属性,该属性指向构造函数Pets,因此打印Pets。
到这里,关于原型继承中涉及到的构造函数、prototype
、constructor
,[[Prototype]]
以及建立出来的对象之间的关系已经所有呈现出来了。来作个总结:
prototype
是构造函数的一个属性,并无什么特殊和神秘的性质。prototype
是由构造函数所建立对象的原型对象,对象的公共方法和属性能够委托到prototype。prototype
之间。(Object.create
创建了对象和原型之间的继承关系,和构造函数没有关系)constructor
是语言自动赋予prototype
的一个属性,其值为构造函数自己。[[Prototype]]
是对象的一个内部属性,是一个指针,指向对象的原型对象,在Safari、Chrome和Firefox下,能够经过__proto__
属性访问。仍是使用上面的例子,咱们将全部这些关键词之间的相互关系使用一个图示展现出来:
如今回过头去看开头提到的那个错误例子,简直就错的离谱啊。这个错误明显神化了prototype
的做用,觉得只要使用了prototype
属性,而后就如同黑魔法通常,在两个彻底不相关的对象之间架起了一座桥梁,也就是继承关系,而后就能够随意使用另一个对象的方法了。天真!
个人问题在于,首先,神话了prototype
的做用。prototype
并无这种黑魔法,它只是一个属性。
其次,没有搞明白继承关系到底存在与哪两个对象之间。(被建立对象和prototype之间)
因此,在错误的代码中,dog对象没有makeSound方法,dog对象继承Object.prototype
,而非pets,而Object.prototype
上并无所谓的makeSound方法,返回undefined,因此报错。
以上,但愿对你有所帮助。