我(我的)不喜欢的,就是讲原型时上来就拿类作比较的,因此我不会这样讲。不过个人确讲过构造器函数,在这方面和类多多少少有共通之处。个人建议是:忘掉类。有不少观点认为“类”学的泛滥是面向对象的过分发展,是一种悲哀,以致于有太多的开发者几乎把面向对象和类划上了等号。在学习原型以前,我请你先记住并品味这句话:javascript
面向对象设计的精髓在于“抽象”二字,类是实现实体抽象的一种手段,但不是惟一一种。java
prototype
和 __proto__
事先声明:永远,永远不要在真实的代码里使用 __proto__
属性,在本文里用它纯粹是用于研究!很快咱们会讲到它的替代品,抱歉请忍耐。git
在 JavaScript 里,函数是对象(等学完了这一篇,不妨研究一下函数到底是怎么就成了对象的?),对象嘛,毫无心外的就会有属性(方法也是属性),而后毫无心外的 prototype
就是函数的一个属性,最后毫无心外的 prototype
属性也是一个对象。瞧,多么瓜熟蒂落的事情:程序员
function foo() {} foo.prototype; // 里面有啥本身去看
好吧,那 prototype
有啥用?呃,若是你把函数就当作函数来用,那它压根没用。不过,若你把函数看成构造器来用的话,新生成的对象就能够直接访问到 prototype
对象里的属性。github
// 要充当构造器了,按惯例把首字母大写 function Foo() {} var f = new Foo(); f.constructor; // function Foo() {}
想一下,f
的 constructor
属性哪里来的?若是你想不明白,请用 console.dir(Foo.prototype)
一探究竟。segmentfault
这说明了一个问题:浏览器
函数的原型属性不是给函数本身用的,而是给用函数充当构造器建立的对象使用的。安全
使人疑惑的是,prototype
属性存在于 Foo
函数对象内,那么由 Foo
建立的实例对象 f
是怎么访问到 prototype
的呢?是经过复制 prototype
对象吗?接着上面的代码咱们继续来看:函数
f.__proto__; // Foo {} Foo.prototype; // Foo {} f.__proto__ === Foo.prototype; // true
哦~不是复制过来的,而是一个叫作 __proto__
的属性指向了构造器的 prototype
对象呀。学习
没错!这就是原型机制的精髓所在,让咱们来总结一下全部的细节(包括隐含在表象之下的):
prototype
属性,可是函数本身不用它new
操做符的配合。其工做原理我已经在第一篇作了大部分的阐述new
在建立新对象的时候,会赋予新对象一个属性指向构造器的 prototype
属性。这个新的属性在某些浏览器环境内叫作 __proto__
__proto__
指向的 prototype
对象),若是尚未就查找原型的原型(prototype
也有它本身的 __proto__
,指向更上一级的prototype
对象),依此类推一直找到 Object
为止OK,上面的第四点事实上就是 JavaScript 的对象属性查找机制。因而可知:
原型的意义就在于为对象的属性查找机制提供一个方向,或者说一条路线
一个对象,它有许多属性,其中有一个属性指向了另一个对象的原型属性;然后者也有一个属性指向了再另一个对象的原型属性。这就像一条一环套一环的锁链同样,而且从这条锁链的任何一点寻找下去,最后都能找到链条的起点,即 Object
;所以,咱们也把这种机制称做:原型链。
如今,我但愿统一一下所使用的术语(至少在本文范围内):
prototype
属性:咱们叫它 原型属性 或 原型对象__proto__
属性:咱们叫它 原型例如:
Foo
的原型属性(或原型对象) = Foo.prototype
f
的原型 = f.__proto__
统一术语的缘由在于,尽管 Foo.prototype
和 f.__proto__
是等价的,可是 prototype
和 __proto__
并不同。当考虑一个固定的对象时,它的 prototype
是给原型链的下方使用的,而它的 __proto__
则指向了原型链的上方;所以,一旦咱们说“原型属性”或者“原型对象”,那么就暗示着这是给它的子子孙孙们用的,而说“原型”则是暗示这是从它的父辈继承过来的。
再换一种说法:对象的原型属性或原型对象不是给本身用的,而对象的原型是能够直接使用的。
__proto__
的问题既然 __proto__
能够访问到对象的原型,那么为何禁止在实际中使用呢?
这是一个设计上的失误,致使 __proto__
属性是能够被修改的,同时意味着 JavaScript 的属性查找机制会所以而“瘫痪”,因此强烈的不建议使用它。
若是你确实要经过一个对象访问其原型,ES5 提供了一个新方法:
Object.getPrototypeOf(f) // Foo {}
这是安全的,尽管放心使用。考虑到低版本浏览器的兼容性问题,可使用 es5-shim
因为对象的原型是一个引用而不是赋值,因此更改原型的属性会马上做用于全部的实例对象。这一特性很是适用于为对象定义实例方法:
function Person(name) { this.name = name; } Person.prototype.greeting = function () { return "你好,我叫" + this.name; }; var p1 = new Person("张三"); var p2 = new Person("李四"); p1.greeting(); // 你好,我叫张三 p2.greeting(); // 你好,我叫李四 /* 改变实例方法的行为:*/ Person.prototype.greeting = function () { return "你好,我叫" + this.name + ",很高兴认识你!"; }; /* 观察其影响:*/ p1.greeting(); // 你好,我叫张三,很高兴认识你! p2.greeting(); // 你好,我叫李四,很高兴认识你!
然而,改变自有属性则不一样,它只会对新建立的实例对象产生影响,接上例:
function Person(name) { this.name = "超人"; } /* 不影响已存在的实例对象 */ p1.greeting(); // 你好,我叫张三,很高兴认识你! /* 只影响新建立的实例对象 */ var p3 = new Person("王五"); p3.greeting(); // 你好,我叫超人,很高兴认识你!
这个例子看起来有点无厘头,没啥大用,不过它的精神在于:在现实世界中,复杂对象的行为或许会根据状况对其进行重写,可是咱们不但愿改变对象的内部状态;或者,咱们会实现继承,去覆盖父级对象的某些行为而不引向其余相同的部分。在这些状况下,原型会给予咱们最大程度的灵活性。
咱们如何知道属性是自有的仍是来自于原型的?上代码~
p1.hasOwnProperty("name"); // true p1.hasOwnProperty("greeting"); // false p1.constructor.prototype.hasOwnProperty("greeting"); // true Object.getPrototypeOf(p1).hasOwnProperty("greeting"); // true
代码很简单,就不用过分解释了,注意最后两句实际上等价的写法。
constructor
刚才的这一句代码:p1.constructor.prototype.hasOwnProperty("greeting");
,其实暗含了一个有趣的问题。
对象 p1
可以访问本身的构造器,这要谢谢原型为它提供了 constructor
属性。接着经过 constructor
属性又能够反过来访问到原型对象,这彷佛是一个圈圈,咱们来试验一下:
p1.constructor === p1.constructor.prototype.constructor; // true p1.constructor === p1.constructor.prototype.constructor.prototype.constructor; // true
还真是!不过咱们不是由于好玩才研究这个的。
尽管咱们说:更改原型对象的属性会当即做用于全部的实例对象,可是若是你彻底覆盖了原型对象,事情就变得诡异起来了:(阅读接下来的例子,请一句一句验证本身心中所想)
function Person(name) { this.name = name; } var p1 = new Person("张三"); Person.prototype.greeting = function () { return "你好,我叫" + this.name; }; p1.name; // 张三 p1.greeting(); // 你好,我叫张三 p1.constructor === Person; // true /* so far so good, but... */ Person.prototype = { say: function () { return "你好,我叫" + this.name; } }; p1.say(); // TypeError: Object #<Person> has no method 'say' p1.constructor.prototype; // Object { say: function }
呃?Person
的原型属性里明明有 say
方法呀?原型对象不是即时生效的吗?
如果只为了建立一种对象,原型的做用就没法所有发挥出来。咱们会进一步利用原型和原型链的特性来拓展咱们的代码,实现基于原型的继承。
原型继承是一个很是大的话题范围,慢慢地你会发现,尽管原型继承看起来没有类继承那么的规整(相对而言),可是它却更加灵活。不管是单继承仍是多继承,甚至是 Mixin 及其余连名字都说不上来的继承方式,原型继承都有办法实现,而且每每不止一种办法。
不过让咱们先从简单的开始:
function Person() { this.klass = '人类'; } Person.prototype.toString = function () { return this.klass; }; Person.prototype.greeting = function () { return '你们好,我叫' + this.name + ', 我是一名' + this.klass + '。'; }; function Programmer(name) { this.name = name; this.klass = '程序员'; } Programmer.prototype = new Person(); Programmer.prototype.constructor = Programmer;
这是一个很是好的例子,它向咱们揭示了如下要点:
var someone = new Programmer('张三'); someone.name; // 张三 someone.toString(); // 程序员 someone.greeting(); // 你们好,我叫张三, 我是一名程序员。
我来捋一遍:
new Person()
建立了对象,而后赋给了 Programmer.prototype
因而构造器的原型属性就变成了 Person
的实例对象。Person
对象拥有重写过的 toString()
方法,而且这个方法返回的是宿主对象的 klass
属性,因此咱们能够给Programmer
定义一个 greeting()
方法,并在其中使用继承而来的 toString()
。someone
对象调用 toString()
方法的时候,this
指向的是它本身,因此可以输出 程序员
而不是 人类
。还没完,继续看:
// 由于 Programmer.prototype.constructor = Programmer; 咱们才能获得: someone.constructor === Programmer; // true // 这些结果体现了何谓“链式”原型继承 someone instanceof Programmer; // true someone instanceof Person; // true someone instanceof Object; // true
上例其实已经实现了对 toString()
方法的重载(这个方法的始祖对象是 Object.prototype
),秉承一样的精神,咱们本身写的子构造器一样能够经过原型属性来重载父构造器提供的方法:
Programmer.prototype.toString = function () { return this.klass + "(码农)"; } var codingFarmer = new Programmer("张三"); codingFarmer.greeting(); // 你们好,我叫张三, 我是一名程序员(码农)。
思惟活跃反应快的同窗或许已经在想了:
为何必定要把父类的实例赋给子类的原型属性,而不是直接用父类的原型属性呢?
好问题!这个想法很是有道理,并且这么一来咱们还能够减小属性查找的次数,由于向上查找的时候跳过了父类实例的 __proto__
,直接找到了(如上例)Person.prototype
。
然而不这么作的理由也很简单,若是你这么作了:
Programmer.prototype = Person.prototype;
因为 Javascript 是引用赋值,所以等号两端的两个属性等于指向了同一个对象,那么一旦你在子类对方法进行重载,连带着父类的方法也一块儿变化了,这就失去了重载的意义。所以只有在肯定不须要重载的时候才能够这么作。