一、工厂模式
用函数来封装以特定接口创建对象的细节。
function createPerson(name,age,job){ var o = new Object(); o.name = name; o.gae= age; o.job = job; o.sayName = function(){ alert(this.name); } return o; } var person1 = createPerson("Nicholas",29,"Software Engineer"); //第一个实例 var person2 = createPerson("Greg",27,"Doctor"); //第二个实例
工厂模式解决了创建多个相似对象的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型)。
二、构造函数模式
function Person (name,age,job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); }; } var person1 = new Person("Nicholas",29,"Software Engineer"); var person2 = new Person("Greg",27,"Doctor");
注意:构造函数要以一个大写字母开头,非构造函数则以一个小写字母开头。
要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数会经历以下4个步骤:
(1)创建一个新对象;
(2)将构造函数的作用域赋给新对象(因此this指向了这个新对象);
(3)执行构造函数中的代码(为这个新对象添加属性);
(4)返回新对象。
好处:创建自定义的构造函数意味着将来可以将它的实例标识为一种特性的类型。
问题:使用构造函数的问题是,每个方法都要在每个实例上重新创建一遍。上例中,每个Person实例都包含一个不同的Function实例(以显示name属性)。因此不同实例上的同名函数是不相等的。
然而,创建两个完成同样任务的Function实例的确没有必要。可以像下面这样,把函数定义转移到构造函数外部。
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("Nicholas",29,"Software Engineer"); var person2 = new Person("Greg",27,"Doctor");
把sayName()函数的定义转移到构造函数外部。在构造函数内部,将sayName属性设置成全局的sayName函数。由于sayName包含的是一个指向函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。
产生新的问题:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而且,如果对象需要定义很多方法,那就要定义很多个全局函数,于是这个自定义的引用类型就丝毫没有封装性可言了。可以通过原型模式解决这个问题。
三、原型模式
原理:每创建一个新函数,就会根据特定的规则为该函数创建一个prototype(原型)属性。prototype属性是一个指针,指向函数的原型对象。默认情况下,所有原型对象会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。拿前面例子来说,Person.prototype.constructor指向Person。原型对象实质上也是构造函数的一个实例对象。
原型对象的好处:可以让所有对象实例共享它所包含的属性和方法。这样,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。而且,如果我们要修改所有实例中的属性和方法,只需要修改一处,就能够影响到所有实例了。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性(其他属性和方法可以自己添加);至于其他方法则都是从Object继承而来(因为所有函数的默认原型都是Object的实例)。当调用构造函数创建一个新实例后,该实例的内部会包含一个指针[[Prototype]](内部属性),指向构造函数的原型对象。要明确:链接存在于实例于构造函数的原型对象之间,而不是存在于实例与构造函数之间。
原型模式代码:
function Person(){ } Person.prototyoe.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function (){ alert(this.name); }; var person1 = new Person(); persion1.sayName(); //"Nicholas" var person2 = new Person(); person2.sayName(); //"Nicholas" alert(person1.sayname == person2.sayName); //true
根据上图可以看出,为何代码“person1sayName==person2.sayName”返回的是“true”,因为sayName方法是有Person构造函数的原型对象定义的,可以由所有Person函数的实例所共享,而不是每个对象实例都各自创建了一个sayName方法。
在原型中添加属性和方法可以参照如下代码:
function User(name.age){ //构造函数 this.name = name; //对象属性 this.age = age; } User.prototype.addr = "湖北武汉"; //在原型中添加属性 User.prototype.show = function(){ //在原型中添加方法 alert (this.name + "|" +this.age); }; var user1 = new User ("ZXC",22); //创建实例 ver user2 = new User ("CXZ",21); user1.show(); //调用show()方法 user2.show(); alert(user1.show == user2.show); //返回true,说明show方法是共享的 alert(user1.addr); //"湖北武汉" alert(user2.addr); //"湖北武汉"
但是有个问题:如果我们既在构造函数中添加了一个属性、又在原型对象中添加了该属性,还在实例中添加了该属性,那么我们访问的究竟是哪一个属性呢?先看下面的代码:
function User(name,age){//构造方法 this.name = name;//对象属性 this.age = age; this.addr = '湖北恩施'; } User.prototype.addr = '湖北武汉';//在原型中添加属性 var user1 = new User('ZXC',22);//创建实例 var user2 = new User('CXZ',21); alert(user1.addr);//'湖北恩施' delete user1.addr;//删除对象属性 alert(user1.addr);//'湖北武汉' delete User.prototype.addr; alert(user1.addr);//'undefined' user2.addr = '武汉'; alert(user2.addr);//'武汉'
可以看出,如果我们同时申明了对象属性、原型属性和实例属性,那么调用时显示的优先级应该是:实例属性>对象属性>原型属性。这就是采用了就近原则:调用时首先查找实例中是否直接定义了这个属性,有则返回实例属性;如果实例属性中没有就去构造函数中查找,有则返回;如果前面两者都没有,就去原型对象中查找,如果没有则返回undefined。
原型模式的问题:如果原型属性中包含一个引用类型值的属性(比如数组),当通过其中一个实例修改该属性值,因为该实例是通过指针指向的该属性,所以这个修改也会在原型属性中反映出来,从而使得所有实例的该属性都修改了。
四、组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
这样就可以解决上面的问题。因为属性在构造函数中,通过构造函数创建了实例后,每个实例都会有自己的一份实例属性副本。通过一个实例修改属性不会反映在另一个实例中。
五、动态原型模式
结合使用得问题是:会觉得上面的代码感觉别扭,因为原型中的方法和属性与构造函数中定义的对象属性和方法不在一块,要是能封装在一起就更加直观,如果要解决这个问题,就要用到动态原型模式。
//动态原型模式 function User(name,age){ //构造方法 this.name = name; //属性 this.age = age; this.addr = '湖北恩施'; User.prototype.addr = '湖北武汉'; //在原型中添加属性 User.prototype.show = function(){ //在原型中添加方法 alert(this.name+'|'+this.age+'|'+this.addr); }; } var user1 = new User('ZXC',22); //创建实例 var user2 = new User('CXZ',21); user1.show(); //调用show()方法 user2.show(); alert(user1.show==user2.show); //返回 true
上面的代码看起来更直观。但有个问题是,如果要创建多个实例,每创建一个实例就会在原型中重新创建一次原型中的方法。
要解决这个问题,思路是:首先判断show方法是否存在,如果不存在则创建,如果已经存在就不创建。改进代码如下:
function User(name,age){ //属性 this.name = name; this.age = age; this.addr = '湖北恩施'; //方法 if (this.show==undefined){ User.prototype.show = function(){ alert(this.name + "|" + this.age + "|" + this.addr); }; } }
if函数这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,show方法已经存在,就不用再添加到原型中,也就不会再创建一个函数了。
六、使用字面量方式创建原型
除了上面提到的创建原型的方式,还可以用字面量方式创建,代码如下:
//使用字面量方式创建原型 function User(name,age){//构造方法 this.name = name;//属性 this.age = age; } User.prototype = { addr : '湖北武汉', show : function(){ alert(this.name+'|'+this.age+'|'+this.addr); } }; var user1 = new User('ZXC',22);//创建实例 var user2 = new User('CXZ',21); user1.show();//调用show()方法 user2.show();
要说明的是:使用字面量方式创建原型后,不能再使用字面量的方式重写原型。因为重写原型,相当于是把原型修改为了另一个对象。如果实例是在重写之前创建的,则该实例扔指向原来的原型对象;如果实例是在重写之后创建的,则实例指向新的原型对象。
但是,可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来。
关于对象和原型,请参考:
1、JavaScript中的对象和原型(一)
2、JavaScript中的对象和原型(二)
3、JavaScript中的对象和原型(三)