YConstructor? YPrototype?

我们在创建对象时经常会用到构造函数和prototype,但是为什么要这样做呢?其中的来龙去脉又是如何?长久以来我也是对此一知半解,读过Nicholas C. Zakas的《JavaScript高级程序设计(第二版)》之后,豁然开朗,在此总结一下。

1.创建简单的对象

 

var person = {};
person.name = 'ningzhongbin';
person.age = '21';
person.job = 'Front-end Engineer';

person.sayName = function() {
    alert(this.name);
};

person.sayName();//'ningzhongbin' 


 

上面的代码创建了一个最简单的对象,很直白明了,早期的JavaScript开发人员经常使用这个模式创建新对象。但是缺点很明显,当我们要创建很多类似的对象时,会产生大量重复的代码。于是人们开始使用下面这种工厂模式的一种变体。

2.工厂模式

由于在JavaScript中无法创建类,于是我们用函数来封装以特定的借口创建对象的细节,如下面例子所示:

 

function createPerson(name, age, job) {
     var o = {};
     o.name = name;
     o.age = age;
     o.job = job;
     o.sayName = function(){
          alert(this.name);
     };
     return o;
}

var person1 = createPerson('ningzhongbin', 21, 'Front-end Engineer');
var person2 = createPerson('Eminem', 30, 'Singer');

person1.sayName();//'ningzhongbin'
person2.sayName();//'Eminem'

现在我们可以随意创建任意个这种person对象,但是,工厂模式虽然解决多个相似对象的问题,但却没有解决对象识别的问题(即怎么知道一个对象的类型),于是,出现了下面这种构造函数的模式。

 

3.构造函数模式

 

function Person(name, age, job){
	this.name = name;
	this.age = age;
	this.job = job;
	this.sayName = function(){
		alert(this.name);
	};
}
			
var person1 = new Person('NingZhongbin', 21, 'Front-end Engineer');
var person2 = new Person('Someone', 99, 'Unknown');
			
person1.sayName();//'NingZhongbin'
person2.sayName();//'Someone'

我们都知道要创建Person实例,必须使用new操作符。那么在new的过程中发生了那些事情呢?有以下几个步骤:

 

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

通过构造函数模式我们很容易解决了工厂模式遗留的对象识别的问题,检测对象类型,我们通常使用instanceof操作符来验证,看下面的代码:

 

alert(person1 instanceof Person); //true person1是Person的实例
alert(person2 instanceof Object); //true 同时也是Object的实例
alert(person1 instanceof Person); //true person2同理
alert(person2 instanceof Object); //true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而正是构造函数模式胜过工厂模式的地方。

 

然而,构造函数模式虽有好用,也并非没有缺点,使用构造函数的主要问题,就是每次实例都要创建一个新对象(这是不可避免的,因为实例就是一个对象),每个方法都要在每个实例上重新创建一遍,这样造成了重复劳动,例如上面的person1和person2的sayName方法,其实我们可以将这个sayName方法抽取出来,统一成一个全局的sayName函数,如下面代码:

 

function Person(name, age, job){
	this.name = name;
	this.age = age;
	this.job = job;
	this.sayName = sayName;
}

function sayName(){
	alert(this.name);
}
			
var person1 = new Person('NingZhongbin', 21, 'Front-end Engineer');
var	person2 = new Person('Someone', 99, 'Unknown');
			
person1.sayName();//'NingZhongbin'
person2.sayName();//'Someone'

这样的确解决了上面所说的问题,person1和person2共享了全局作用域下的同一个方法sayName,但是此时新问题又来了:我们在全局作用域中定义的方法只是用来被Person的实例调用,这让全局作用域有点名不副实,我们构造函数也丝毫没有封装性可言。于是,就出现了原型模式。

 

4.原型模式

我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的实例和方法,有点拗口,我们可以这样理解,prototype对象里的属性和方法类似于全局作用域中的变量和函数,只不过只能被该构造函数的实例所共享,既然是共享,那么我们要知道,prototype的属性和方法只要在一个实例上被修改,就会在所有实例上反映出来,就类似于全局变量,于是一般的情况我们都只把方法挂在prototype对象上,而把属性写在构造函数里面,由构造函数实例化时创建,作为每个实例之间不同的私有的属性。如下面代码:

 

function Person(name, age, job){
	this.name = name;//属性由构造函数来创建
	this.age = age;
	this.job = job;
}
//通用的方法放在prototype里面
Person.prototype.sayName = function(){
	alert(this.name);
};
			
var person1 = new Person('ningzhongbin', 21, 'Front-end Engineer');
var person2 = new Person('Eminem', 30, 'Singer');
			
person1.sayName();//'ningzhongbin'
person2.sayName();//'Eminem'

这样我们解决了构造函数模式的问题,每次实例化的时候只创建属性,把通用方法抽取出来给实例共享。这就是为什么要用prototype的原因。当然,原型对象prototype的内容不知这些,本文旨在粗略过一下几种创建对象的模式及比较他们的优缺点,原型prototype的其他作用及相关内容下篇文章再作讨论。

 

 

结束语:有时候,我们做一件事情,不仅要知道怎么做,还要知道为什么要做,为什么这样做?编程如此,人生也是如此!

posted @ 2010-12-22 17:52  akakingback  阅读(207)  评论(0编辑  收藏  举报