一切从函数开始javascript原型真是一个令人望而生畏的字眼,很多时候,一谈到原型,整个气氛就瞬间严肃起来。的的确确,原型是这门语言的精华,但也是面试题目中的老戏骨——它对新手总是不那么友好。在这篇文章中,作者以轻松幽默的语调,举一反三,循序渐进的将javascript原型的相关知识依依为你揭晓。但是,你不要断章取义,因为作者善于在后面说明前面事例的不足,更善于先用蹩脚的例子告诉你问题的本质,再提出优秀的解决方案。这是你在阅读时需要注意的。
本章不涉及原型继承的相关知识,因为作者认为应该把继承单独抽出组成一章,如果你对继承的相关知识和作者的风格感兴趣,继续读下一篇博客《轻松理解javascript继承》。
这篇文章参考了《javascript语言精粹》、《javascript启示录》以及《javascript权威指南》的内容。
在javascript中,函数是对象,我们可以把函数存储在一个变量中,也可以给函数添加属性。JS中所有的函数都由一个叫做Function的构造器创建。当一个函数对象被创建时,Function构造器会"隐蔽地"给这个函数对象添加一个叫做prototype的属性,其值是一个包含函数本身(constuctor)的对象:
this.prototype = {constructor : this}
其中,prototype就是“传说中”的原型,而的this指的就是函数本身。javascript会“公平地”为每个函数创建原型对象。无论这个函数以后是否用作构造函数。
下面的代码是个很好的例子:
function sayHello () {
}
console.log(sayHello.prototype) //=> { constuctor : sayHello(), __proto__ : Object}
你会发现还有一个叫做__proto__
的属性,这又是什么呢?先不要乱了阵脚,继续向下看。
当函数“有志气”成为一名构造函数的时候,prototype属性开始真正发挥作用。new运算符是一名优秀的“工匠”,它可以使用对象模具——构造函数产生一个个的实例对象。
当new运算符使用构造函数产生对象实例时,会“强制性地”在新对象中添加一个叫做__proto__
的属性作为”隐秘连接“,它的值就等于它的构造函数prototype属性的值,换句话说,使这它与其构造函数的prototype属性指向同一个对象。
显然,每一个javascript对象都会拥有一个叫做__proto__
的属性,因为javascript中所有的对象都隐式或显式地由构造函数new出,于是,也可以说在javscript中没有真正意义上的空对象。
当然,我们的new运算符没有忘记它的“老本行”:它会将构造函数中定义的实例属性或方法(this.属性)添加到新创建的对象中。
下面的代码或许能够帮助你理解:
function Student (name) {
this.name = name;
}
// 为构造器的prototype新增一个属性
Student.prototype.age = 20;
var Tom = new Student("Tom");
console.log(Tom.name); // => Tom
console.log(Tom.__proto__.constructor); // =>function Student() {this.name = name}
console.log(Tom.__proto__.age); // =>20
简而言之,原型prototype是javascript函数的一个属性,当这个函数作为构造器产生实例时,new运算符会获得函数的prototype属性的值并将其赋给对象实例的__proto__
属性,并以此作为隐秘连接。因此,你在构造函数的prototype属性中设置的值都会被该构造器的实例所拥有。
之所以还不说原型链,是因为我想先试着不把事情变得那么复杂:还是以上面的Student伪类为例。Tom对象的__proto__
属性来自其构造器Student的prototype属性,这个应该很好理解。但是,问题是Student的prototype也是一个对象,它有我们设置的age属性,更有每个对象都拥有的__proto__
属性。那么问题来了,Student的prototype对象是谁创建的呢,它的__proto__
值从来自哪里呢?
Object构造器是无名英雄——它创建所有以对象字面量表示的对象。Student的prototype对象正是由Object构造器创建的,它的__protot__
值是在Object构造器的prototype属性。
希望下面的例子能够帮助你理解:
var obj = {};
console.log(obj.constructor); // =>function Object() {native code}
console.log('__proto__' in obj); // =>true
灵魂连接——原型链
好的,原型链在我们试图从某个对象获取某个属性(或方法)时发挥作用。如果那个属性刚好像下面这样存在于这个对象之中,那无需多虑,直接返回即可。
var student = {name : 'Jack'}
student.name // =>Jack
但是,如果这个属性不直接存在于这个对象中,那么javascript会在这个对象的构造器的prototype属性,也就是这个对象的__proto__
属性中进行查找。
由于访问__proto__
并非官方ECMA标准的一部分,所以后面我们都说”其构造函数的prototype属性”,而不说“这个对象的__proto__
属性“了。
好吧,如果找到,则直接返回,否则,继续这个循环,因为prototype的值也是对象:继续在 /该对象的构造器的prototype对象/ 的构造器的prototype属性中寻找……。
所以你该知道,由于prototype属性一定是一个对象,因此原型链或者说查找中的最后一站是Object.prototype。如果查找到这里仍然没有发现,则循环结束,返回undefined。
因为这种链查找机制的存在,上面的代码得到了简化,这也是Javascript中继承的基石:
console.log(Tom.__proto__.age); // =>20
console.log(Tom.age); // =>20
好吧,我希望通过下面的例子带你拉通走一遍:
var arr = [];
console.log(arr.foo); //=>undefined
首先,当JS得知要访问arr的foo属性时,他首先会在arr对象里查找foo属性,但是结局令人失望。之后,它会去查找arr的构造函数即Array的prototype属性,看是否能在这里查找到什么线索,结果也没有。最后,它会去查找Array的prototype对象的构造函数——Object的prototype属性——仍然没有找到,搜索结束,返回undefined。
之所以举一个原生的构造函数的例子是因为我一直害怕因为使用自定义的例子而给大家带来一种只有自定义的构造函数才可以这样的错觉。你要知道,这篇文章所讲述的道理适合一切的构造器。
好了,让我们看一个自定义的构造器并在原型链上查找到属性的”好“例子:
Object.prototype.foo = "some foo";
function Student(name) {
this.name = name;
}
// 为构造器的prototype新增一个属性
Student.prototype.age = 20;
var Tom = new Student("Tom");
console.log(Tom.name); // => Tom
console.log(Tom.age); // =>20
console.log(Tom.foo); // =>some foo
这里要说明的是,原型链在查找时,会使用它查找到的第一个值;一旦找到,立即返回,不会再往下进行寻找。
私有与共享当属性涉及到对象时问题变得棘手起来,你知道,对象的传递与复制时引用传递,也就是说,我在实例的构造器prototype上设置的属性如果是对象,那么所有实例实际上都将共享这一个对象:
var pocketMoney = {
num: 5,
value:10
};
function Student(name) {
this.name = name;
}
// 为构造器的prototype新增一个属性
Student.prototype.pocketMoney = pocketMoney;
var Tom = new Student("Tom");
var Jack = new Student("Jack");
Tom.pocketMoney.num = 4;
console.log(Tom.pocketMoney.num); // =>4
console.log(Jack.pocketMoney.num); // =>4
Tom去游戏厅打游戏花费了一张10元的纸币,但Jack的零花钱却也因此减少了,这显然是不可接受的。
所以,为了避免这种情况的发生,请将属性写在构造器内部,更改后,事情才看上去像回事:
function Student(name) {
this.name = name;
this.pocketMoney = {
num: 5,
value: 10
};
}
var Tom = new Student("Tom");
var Jack = new Student("Jack");
Tom.pocketMoney.num = 4;
console.log(Tom.pocketMoney.num); // =>4
console.log(Jack.pocketMoney.num); // =>5
我们常常把方法写在prototype中,而不是属性。
阶段总结到此,原型的基础知识我们就已经基本讲完了。如果你觉得有很多东西没有提到,你是正确的,因为我已经把继承单独抽出来,组成了新的一章,里面会涉及大量有关原型的知识,我也会介绍给你一种全新的,备受推崇的继承方式——应用模块的差异化继承。我强烈推荐你趁热打铁,现在就去读我博客中的《轻松理解javascript继承》