翻译自http://javascriptissexy.com/javascript-prototype-in-plain-detailed-language/

Prototype是每一位JavaScript程序员都应该了解的基本概念,本文就将用朴素详细的语言来解释JavaScript的原型。如果在阅读本文之后,你还有不了解的地方,请在下面的评论区询问我。我会逐个回答你们的问题。

要了解JavaScript的原型,首先你必须了解JavaScript的对象。如果你对对象还不熟悉,你应该先阅读这篇文章《详细了解JavaScript的对象》。同时,也要知道,property是定义在函数上的一个变量。


在JavaScript里,有两个与prototype相互关联的概念:

1、第一,JavaScript的每一个函数都有原型属性,这个属性默认为空。当你需要实现继承的时候,就把属性和方法添加到这个原型属性上。prototype属性是不可枚举的,换句话说,不能使用for...in循环来访问这些属性。但是,Firefox和大多数版本的Safari以及Chrome都有一个__proto__的伪属性。这个属性允许你访问一个对象的原型属性。你可能从来没有使用过这个属性,但是,你应该知道它的存在,并且,在一些浏览器中,用它来访问对象的属性是一种很简便的方法。

prototype属性主要用来实现继承。把属性和方法添加到一个函数的prototype上以使这个函数的实例也能访问这些属性和方法。

看看下面这个使用prototype实现的简单继承的例子(后面会讲到更多的继承):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function PrintStuff( myDocuments ) {
this.documents = myDocuments;
}
/* 我们把print()方法添加到PrintStuff的原型属性上,所以其他实例或者对象能够继承这个方法 */
PrintStuff.prototype.print = function () {
console.log(this.documents);
}
/* 使用PrintStuff()构造函数创建一个新的对象,这样,就允许这个新的对象继承PrintStuff的属性和方法了 */
var newObj = new PrintStuff('I am a new Object and I can print.');
/* newObj从PrintStuff函数中集成所有的属性和方法,包括print方法。所以,现在即使没有在newObj上创建一个print方法,newObj也可以直接使用print这个方法来打印输出了 */
newObj.print(); // I am a new Object and I can print.

2、第二个与原型有关的属性就是prototype属性了。prototype属性作为对象的一个特征,能够告诉我们这个对象的“父级对象”。简单说来就是:一个对象的原型属性指向它的“父级对象”,这个对象的属性就是从这个“父级对象”继承而来。原型属性通常被作为原型对象,当你创建一个新对象的时候,它自动的就被设置好了。解释这个现象:每一个对象都是从其他的对象继承而来,而这个所谓的“其他对象”正是这个对象的原型属性或者说“父级对象”(你可以把原型属性认为是家族关系或者亲子关系)。在上面的代码例子中,newObj的原型(newObj.prototype)就是PrintStuff.prototype

注意:所有的对象都有属性,就像对象的属性有属性一样。对象的属性包括prototypeclass以及可扩展的属性。我们将在第二个例子中讲到prototype属性。
同时也要注意,__proto__这个伪属性包含了对象的原型对象。


Constructor

在继续之前,我们来简要地介绍一下constructor属性,即构造函数。构造函数是用来初始化一个新对象的函数,使用new这个关键词就可以调用构造函数。

例如:

1
2
3
4
> function Account() {}
// 使用Account构造函数来创建userAccount对象
var userAccount = new Account();
>

另外,所有从另一个对象上继承下来的对象都有一个constructor属性。这个属性与变量类似,保存着或者指向这个对象的构造函数。

1
2
3
4
5
6
7
8
9
> // 这个例子中的构造函数是Object()​
var myObj = new Object();
// 可以使用constructor来查看构造函数
console.log(myObj.constructor); // Object()​
// 这个例子中的构造函数是Account()
var userAccount = new Account ();
console.log(userAccount.constructor); // Account()
>

使用new Object()或者对象字面量的方式创建的原型。

所有的使用对象字面量以及Object()构造函数创建的对象都继承自Object.prototype。因此,Object.prototype就是这些对象的原型属性。Object.prototype自己不继承任何其他对象的属性和方法。

1
2
3
4
5
6
7
/* userAccount继承自Object所以他的原型就是Object.prototype */
var userAccount = new Object();
/* 下面这个例子示范了使用对象字面量来创建userAccount对象。同样的,userAccount也继承自Object,所以它的原型也和上面方式所创建的是一样的。 */
var userAccount = {
name: 'Mike'
}

使用构造函数创建的原型

使用new关键字以及除了Object()的构造函数来创建对象,这个对象的原型从构造函数而来。

例如:

1
2
3
4
function Account () {}
var userAccount = new Account()
/* 使用Account()构造函数来初始化userAccount,所以,它的原型就是Account.prototype */

类似的,任何数组比如var myArray = new Array(),从array.prototype获取原型,并且继承Array.prototype的属性。

所以,当一个对象被创建的时候,它的原型设置有两种方式:

1、如果一个对象是采用对象字面量的方式创建的,比如var newObj = {},他的属性从Object.prototype继承而来,我们就说他的原型就是Object.prototype

2、如果一个对象是使用构造函数创建的,比如new Object()new Fruit()new Array()或者任何其他的,他的属性就继承自这个构造函数(Object(),Friut(),Array())。例如,使用Fruit()这个构造函数,每一次我们创建一个它的新的实例的时候(var aFruit = new Fruit()),新实例的原型就被分配为了这个构造函数的原型,也就是Fruit.prototype。任何使用new array()创建的对象的就会以Array.prototype 作为它的原型。使用new Fruit()创建的对象就会以Fruit.prototype作为它的原型。任何使用对象构造函数创建的对象(比如var anObj = new Object())将继承自Object.prototype

还有很重要的一点,必须知道,在ECMAScript 5里,还可以使用Object.create()来创建对象。这个方法允许你设置对象的原型。我们将在后面的文章讲到ECMAScript 5。


原型为什么这么重要?什么时候该使用它呢?

在JavaScript里,原型有两种重要的使用方式,正如上面所提到的一样:

1、基于原型的继承

在JavaScript里,原型之所以那么重要,是因为JavaScript没有其他面向对象的语言的经典的类继承,因此,在JavaScript里,所有的继承都是通过原型属性才使其成为可能。JavaScript有基于原型继承的机制。继承的概念是对象(在其他语言里称作类)能够从其他的对象(或者类)中继承属性和方法。在JavaScript里,使用原型属性实现继承。例如,你可以创建一个Fruit函数(也就是一个对象,因为在JavaScript里,所有函数也是对象)然后为这个函数的原型添加一些属性和方法,那么,这个函数的所有实例都将会继承这个函数的属性和方法。

JavaScript的继承范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function Plant() {
this.country = 'Mexico';
this.isOrganic = true;
}
// 为Plant的原型添加showNameAndColor方法
Plant.prototype.showNameAndColor = function () {
console.log('I am a ' + this.name + ' and my color is ' + this.color);
}
// 为Plant的原型添加​amIOrganic方法
Plant.prototype.amIOrganic = function () {
if ( this.isOrganic ) {
console.log("I am organic, Baby!");
}
}
function Fruit( fruitName, fruitColor ) {
this.name = fruitName;
this.color = fruitColor;
}
/* 将Fruit的原型设置为Plant的构造函数,这样,就可以继承Plant.prototype的所有方法和属性了 */
Fruit.prototype = new Plant();
// 使用Fruit构造函数创建一个新的对象,aBanana
var aBanana = new Fruit('Banana', 'Yellow');
/* 在这里,aBanana使用aBanana的原型(也就是Fruit.prototype)的name属性, */
console.log(aBanana.name); // Banana​
/* 使用从Fruit上(也就是Plant.prototype)继承下来的showNameAndColor方法。aBanana对象继承了Plant和Fruit的所有属性和方法 */
console.log(aBanana.showNameAndColor()); // I am a Banana and my color is yellow.

注意showNameAndColor这个方法是被aBanana所继承的,尽管这个方法定义在原型链的顶端,也就是Plant.prototype

事实上,任何使用Fruit()构造函数创建的对象都会继承Fruit.prototype上的所有属性和方法,并且,这些属性和方法,全部来自于Plant.prototype。这就是JavaScript中实现继承最重要的原则,并且,在这个过程中,原型链是必须的。

想要更深入的了解JavaScript的面向对象编程,可以去购买Nicholas Zakas的这本书:《Principles of Object-Oriented Programming in JavaScript》

2、访问对象属性

使用原型来访问对象的属性和方法也很重要。任何对象的原型都是一个“父级对象”,所有的属性最初都定义在这里。打个不合适的比方,你从你父亲那儿继承了姓,那么你的父亲就是你的“父级对象”。如果我们要找到你的姓是从哪儿来的,我们先要看看是不是你自己创建的,如果不是,那么,就要看是否是从你的父亲上继承的,如果也不是你的父亲所创建的,那么就要继续搜寻到你父亲的父亲。

类似的,如果你想要访问一个对象的属性,属性的查找就会从对象上开始。如果JS运行时不能在本对象上找到这个属性,那么,他就会查找这个对象的原型(即这个对象继承他的属性的地方)。如果在对象的原型上也没找到,那么查找就会继续到对象的原型的原型(也就是对象的父亲的父亲——祖父)。这个过程会一直持续,直到再也没有更多的原型可以查找(即没有更多的祖父,曾祖父啥的或者没有更多的家系了)。这一过程实质上就是原型链:从一个对象的原型到这个对象的原型的原型并且一直向前的一条线。JavaScript使用原型链来查找对象的属性和方法。

如果对象的原型链上不存在某个属性,那么这个属性将返回undefined

原型链机制本质上和我们上面所说的基于原型的继承是相同的概念。只是我们现在关注的地方在于JavaScript是怎么通过原型来访问对象的属性和方法的。

下面这个例子示范了对象的原型链:

1
2
3
4
5
6
7
8
9
10
11
var myFriends = {
name: 'Pete'
};
/* 为了找到name这个属性,查找会直接从myFriends这个对象上开始,并且马上就找到了这个属性。因为我们在这个对象上就定义了这个属性。可以认为这个原型链只有一个节点。 */
console.log(myFriends.name);
/* 同样的,下面查找toString()这个方法的过程也会从对象本身开始。但是由于我们没有在对象上定义这个方法,所以,编译器会继续在他的原型上去查找这个方法。*/
/* 由于所有使用对象字面量创建的对象都继承自Object.prototype,所以toString方法会在Object.prototype上被找到。*/
myFriends.toString ();

所有的对象都继承自Object.prototype

在JavaScript中,所有的对象都继承了Object.prototype的属性和方法。这些继承下来的属性和方法上包括:constructor,hasOwnProperty(),isPrototypeOf(),propertyIsEnumerable(),toLocalString(),toString(),ValueOf()。ECMAScript 5还在Object.prototype上增加了4种访问器方法。

再来看一个原型链的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function People() {
this.superstar = 'Michael Jackson';
}
/* 在People的原型上定义athlete属性,所有使用People()构造函数创建的对象都可以访问这个属性 */
People.prototype.athlete = 'Tiger Woods';
var famousPerson = new People();
famousPerson.superstar = 'Steve Jobs';
​​
/* 对superstar的查找会首先查看famousPerson对象,我们在这上面定义了,所以这个属性就会被使用了。由于我们在famousPerson对象上重写了superstar属性,所以,查找过程不会再继续往上查找了 */
console.log(famousPerson.superstar); // Steve Jobs​
/* 在ECMAScript 5里,可以设置一个属性为只读,这样,就不能和往常一样对他进行读写了 */
/* 下面这个例子会展示famousPerson的原型上的athlete属性,因为在famousPerson这个对象上,我们没有定义这个属性 */
console.log(famousPerson.athlete); // Tiger Woods​
/* 在这个例子中,查找过程直到了原型链的顶部(Object.prototype)并且找到了toString()方法,正如我们之前的Fruit继承所说的那样,所有对象最重继承自Object.prototype */
console.log(famousPerson.toString()); // [object Object]

所有的内建构造函数(Array(),Number(),String()等)都是从Object()这个构造函数上创建的,所以,它们的原型就是Object.prototype

延伸阅读:

需要了解关于JavaScript对象更多知识,可以阅读David Flanagan的《JavaScript权威指南(第六版)》的第六章。

好好学习,好好睡觉,享受编码的乐趣!