原型(prototype)是每个JavaScript开发人员必须理解的基本概念,本文的目标是通俗而又详细地解释JavaScript的原型。如果你读完这篇博文以后还是不理解JavaScript的原型,请将你的问题写在下面的评论里,我本人会回答所有的问题。
为了理解JavaScript中的原型,你必须理解JavaScript的对象。如果你对对象还不熟悉,你需要阅读我的文章JavaScript Objects in Detail(译文:详解JavaScript对象)。而且你要知道属性就是函数中定义的变量。
在JavaScript中有两个互相之间有关联的原型的概念:
1.首先,每一个JavaScript函数有一个原型属性,当你需要实现继承的时候你就给这个原型属性附加属性和方法。注意这个原型属性是不可以枚举的:它在for/in循环中是不可获取的。但是FireFox和大多数版本的Safari和Chrome浏览器有一个__proto__“伪”属性(一种选择方式)允许你访问对象原型的属性。你可能从来没有用过这个_proto__伪属性,但你得知道它的存在并且它是在某些浏览器中访问对象的原型属性的一种简单的方法。
对象原型主要用于继承:你为对象的原型属性增加方法和属性,使这些原型和属性存在于该函数的实例。
以下是一个简单的使用原型属性继承的例子(后面还有更多关于继承的内容):
function PrintStuff (myDocuments) { this.documents = myDocuments; } //我们为PrintStuff的原型属性增加方法print (),这样的话其他实例(对象)可以继承这个方法 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,即使我们从来没有为它定义过方法print()。 newObj.print (); //I am a new Object and I can print.
2. 第二个关于JavaScript原型的概念是原型特性。把原型特性想成该对象的一个性质;这个性质表明了该对象的“父母”。简而言之:对象的原型特性可以看成是对象的“父母”--该对象获得它属性的地方。对象特性常常被称为原型对象,并且它是在你创建对象时就自动建立的。对此的解释是:每一个对象从某个其他的对象那里继承属性,而这里的“某个其他的对象”就是该对象的原型特性或者“父母”。(你可以把原型特性想象成血缘关系或者父母)。在上面例子的代码中,newObj的原型是PrintStuff.prototype。
注意:所有的对象都有特性,就和对象属性有自己的特性一样。对象特性是原型、类和扩展特性。这些是我们在第二个例子中要讨论的原型特性。
还有一点需要注意, “伪”属性__proto__包含一个对象的原型对象(被该对象继承方法和属性的父对象)。
//重要提示 //构造函数 //在我们继续往下阅读之前,让我们来简单的考查一下构造函数。构造函数是一个用来初始化新对象的函数,并且使用新的关键字来调用构造函数。 //例如: function Account () { } //这是利用构造函数Account来创建对象userAccount var userAccount = new Account (); 而且,所有继承自另一个对象的对象,也继承了那个对象的构造函数属性。而这个构造函数属性就是一个保存或者指向该对象的构造函数的属性(和任何变量一样)。 //本例的构造函数是Object () var myObj = new Object (); //而如果你之后想要知道myObj的构造函数: console.log(myObj.constructor); // Object() // 另一个例子: Account ()是构造函数 var userAccount = new Account (); //查看对象userAccount的构造函数 console.log(userAccount.constructor); // Account()
用new Object()或对象式创建的对象的原型特性
所有用对象式或者构造函数Object创建的对象都继承自Object.prototype。因此Object.prototype是所有用Object()或者{}所创建的对象的原型特性(或原型对象)。Object.prototype本身没有从其他任何对象那里继承任何的方法或者属性。
// 对象userAccount 继承自Object 并且因此它的原型特性就是Object.prototype. var userAccount = new Object (); // 这个声明用了对象式来创造对象userAccount;该对象userAccount继承自Object;因此,就和上面的对象userAccount一样,它的原型特性是Object.prototype。 var userAccount = {name: “Mike”}
用构造函数所创建的对象的原型特性
用新关键字以及任何一种非Object()的构造函数所创建的对象,从该构造函数中获得它们的构造函数。
例如:
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 ()或者 new Anything ()创建的,那么它继承自构造函数 (Object (), Fruit (), Array (), or Anything ())。例如,用一个函数,比如Fruit (),每次我们创建一个新的水果的实例(var aFruit = new Fruit ()),那么该新实例的原型就来自于构造函数Fruit,也就是 Fruit.prototype。任何用new Array ()所创建的对象都会将Array.prototype作为它的原型。任何用构造函数Object(Obj (), 比如 var anObj = new Object() )创建的对象继承自Object.prototype。
还有一点你需要知道的,在ECMAScript 5中,你可以用一个允许你指定新对象的原型的方法Object.create()来创建对象。我们会在后续的文章中学习ECMAScript 5。
原型为什么重要以及何时使用原型?
在JavaScript中原型有两种重要的用途,就像前文中提到的那样:
1.原型属性:基于原型的继承
在JavaScript中原型之所以重要是因为JavaScript没有(大多数面向对象的语言所有的)经典的基于类的继承,因此JavaScript所有的继承是通过原型属性来实现的。JavaScript有一套基于原型继承的机制。继承是一种能让对象(或者是其它语言中的类)继承其它对象(或类)的属性和方法的编程规范。在JavaScript中,通过原型来实现继承。例如,你可以创建一个Fruit函数(也就是对象,因为所有JavaScript中的函数都是对象)并且给这个Fruit的原型属性添加属性和方法,那么所有Fruit函数的实例会继承Fruit全部的属性和方法。
JavaScript中的继承示例:
function Plant () { this.country = "Mexico"; this.isOrganic = true; } //把方法showNameAndColor添加到Plant原型属性 Plant.prototype.showNameAndColor =function () { console.log("I am a " + this.name + " and my color is " + this.color); } // 把方法amIOrganic添加到Plant原型属性 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是在对象Plant.prototype的原型链上定义的,但是此方法还是被对象aBanana所继承。
实际上,任何使用构造函数Fruit ()的对象,都将继承Fruit.prototype全部的属性和方法以及来自Fruit的原型Plant.prototype的全部的属性和方法。这就是JavaScript中实现继承的主要方式以及原型链在这一过程中所扮演的整合角色。
2.原型特性:获取对象的属性
原型对于获取对象的方法和属性也是很重要的。原型特性(或原型对象)是那些可继承的属性的“父母”对象,这些可继承的属性原本就是为这些“父母”对象定义的。这就有点类似于你可以从你的父亲--他是你的“原型父母”,那里继承姓。如果我们想知道你的姓是从哪里来的,我们会先看看是否是你自己给自己取了这个姓;如果不是,我们会继续查看是否你是从你的父亲那里继承了这个姓。如果这个姓不是你父亲的,那么我们会继续查看你父亲的父亲的姓(你父亲的原型父亲)。
与之类似的,如果你想要获取一个对象的原型,你将直接从该对象的属性开始寻找。如果JS运行时不能再那里找到该属性,那么它会去该对象的原型--该对象得到属性的地方,去查看这个属性。
如果在对象的原型中没有发现该属性,那么对于该属性的搜寻会转移到对象的原型的原型(对象的父亲的父亲--爷爷)那里去。就这样一直持续到没有原型为止(没有更多的曾祖父;没有更多有遗传来的血缘关系)。这其实就是原型链:从对象的原型到对象原型的原型不断向上的一条链。并且JavaScript就用这条原型链来搜寻对象的属性和方法。
如果某个属性在它的整条原型链上的任何一个对象的原型中均不存在,那么这个属性就是不存在并且会返回undefined。
这种原型链机制本质上和我们上面讨论的基于原型的继承是一样的概念,只是在这里我们更注重于JavaScript如何通过对象原型获取对象的属性和方法。
这个例子演示了对象的原型对象的原型链:
var myFriends = {name: "Pete"}; //为了找到下面的属性name,搜寻会直接从对象myFriends开始,并且会立刻找到属性name,因为我们为对象myFriends定义了属性name。这个可以被想像成有一条链接的原型链。 console.log(myFriends.name); //在这个例子中,将会从对象myFriends开始搜寻方法toString (),但是因为我们从来没有为对象myFriends创建过方法toString,编译器会接着去myFriends的原型(被myFriends继承属性的那个对象)搜寻。 //并且因为所有用对象式创建的对象都继承自Object.prototype,方法toString将在 Object.prototype中被发现--关于所有继承自Object.prototype的属性,请看下面的重要提示 myFriends.toString ();
重要提示
所有对象都会继承的Object.prototype的属性
在JavaScript中所有对象的属性和方法继承自Object.prototype。这些继承来的属性和方法有构造函数,hasOwnProperty (), isPrototypeOf (), propertyIsEnumerable (), toLocaleString (), toString (), and valueOf ()。ECMAScript 5中还新增了四种访问Object.prototype的方法。
下面是另一个原型链的例子:
function People () { this.superstar = "Michael Jackson"; } // 为People原型定义属性"athlete"以便"athlete" 可以被所有使用构造函数People () 的对象所访问。 People.prototype.athlete = "Tiger Woods"; var famousPerson = new People (); famousPerson.superstar = "Steve Jobs"; //对于superstar的搜寻将首先查看对象famousPerson是否有属性superstar,而因为就是在那里定义的这个属性,这就是需要用到的属性。因为我们已经为对象famousPerson重复定义链famousPerson的属性superstar,所以对于superstar的搜寻就不会在原型链上继续上升。 console.log (famousPerson.superstar); // Steve Jobs // 注意在ECMAScript 5中你可以将属性设置为只读,这样的话你就不能像我们刚才那样重复定义该属性。 //这里展示了来自famousPerson原型(People.prototype)的属性,因为属性athlete没有为对象famousPerson本身所定义。 console.log (famousPerson.athlete); // Tiger Woods //在这个例子中,在原型链上向上搜寻并且在Object.prototype中找到了方法toString,这个方法来自对象Fruit的继承--像我们前面提到的那样,所有的对象最终继承自Object.prototype console.log (famousPerson.toString()); // [object Object]
所有已经建立的构造函数 (Array (), Number (), String (), etc.)都是由构造函数Object所创建的,因此它们的原型是Object.prototype。