参考资料《JavaScript高级程序设计(第2版)》
ECMA把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。” 我们可以把ECMAScript的对象想象成散列表:无非就是一组名值对,其中值可以是数据或者函数。
看了别人N多JavaScript的代码,有各种各样的对象创建模式,下面讲一下几种模式:
1.原始的创建方法,也是最容易理解的
var person = new Object();
//创建person对象
person.name="Nicholas";
person.age=29;
person.job="Software Engineer";
//添加三个属性(name,age,job)和一个方法(sayName())
person.sayName = function(){
alert(this.name);
};
person.sayName();
N年前开发人员就是用这种,现在几乎看不到了,因为它有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
2.工厂模式
考虑到ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下面的例子所示:
function createPerson(name,age,job){
var o=new Object();
o.name = name;
o.age = 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");
person1.sayName();//"Nicholas"
person2.sayName();//"Greg"
说明:函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象 。可以无数次的调用这个函数,而每次它都会返回 一个包含三个属性一个方法的对象。这个模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题,即怎样知道一个对象的类型。随着JavaScript的发展,又一个新模式出现了。
3.构造函数模式
ECMAScript中的构造函数可用来创建特定类型的对象。上面的例子重写如下:
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");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
我们看到Person()中的代码与前一个的不同之处:
- 没有显式地创建对象;
- 直接将属性的方法赋给了this对象;
- 没有return语句。
另用这个函数创建对象的步骤如下:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
person1和person2分别保存着Person的一个不同的实例。检查对象类型可以用instanceof,如
alert(person1 instanceof Object); //true所有对象均继承自Object
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。
另构造函数也是函数,如果不用neww操作符来调用,那它跟普通函数没什么两样,任何函数只要用new操作符来调用,那它就可以作为构造函数。
如下示例:
//当作构造函数使用
var person = new Person("Nicholas",29,"Software Engineer");
person.sayName(); //"Nicholas"
//作为普通函数调用
Person("Greg",27,"Doctor");//添加到window
window.sayName(); //"Greg"
//在另一个对象的作用域中调用
var o=new Object();
Person.call(o,"Kristen",25,"Nurse");
o.sayName(); //"Kristen"
构造函数的问题
构造虽然好用,但也并非没有缺点。主要缺点就是每个方法都要在每个实例上重新创建一遍。如前面的例子,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。ECMAScript中的函数是对象,因此每定义一个函数就实例化了一个对象。如alert(person1.sayName == person2.sayName);//false。这很明显没有必要有两个相同功能的实例,正因为这样,可以通过把函数定义移到构造函数外部来解决这个问题:
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");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
alert(person1.sayName == person2.sayName);//true
这样以来呢,新问题又来了,在全局作用域上定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。还有如果对象有很多方法,那么就要定义很多全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。于是出现了原型模式。
4.原型模式
我们每创建的函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法 。prototype就是通过调用构造函数而创建的那个对象的原型对象。它可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName= function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName);//true
在此,我们将sayName()方法和所有属性直接添加到了Person的prototype属性中,构造函数成了空函数。这样person1和person2访问的都是同一组属性和同一个sayName()函数。
我们发现对象实例person1和person2中的属性完全一样,如何想要不一样怎么办?
我们虽然不可能通过对象实例重写原型中值。但可以添加新的属性,当新的属性与原型的中属性同名时,那么实例中的属性将会屏蔽原型中的那个属性。如下所示:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName= function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"来自实例
alert(person2.name); //"Nicholas"来自原型
需要注意,一旦屏蔽原型中的值,想要再访问原型中属性,即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。只能使用delete操作符将实例属性完全删除。
delete person1.name; //删除实例中的属性
alert(person1.name); //Nicholas 来自原型
可以使用hasOwnProperty()方法判断一个属性是存在于实例中还是原型中,
person1.hasOwnProperty("name"); //当存在于实例中时返回true
更简单的原型语法
为减少不必要的输入,可以用一个包含所有属性和方法的对象字面量来重写整个原型对象,如:
function Person(){
}
Person.prototype = {
name:"Nicholas",
age : 29,
job:"Software Engineer",
sayName : function(){
alert(this.name);
}
};
以上我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外:constructor属性不再指向Person了
var person = new Person();
alert(person instanceof Object); //true
alert(person instanceof Person); //true
alert(person.constructor == Object); //true
alert(person.constructor == Person); //false
如果constructor真的很重要,可以特意将它设置回适当的值:
...
Person.prototype = {
constructor : Person,
...
原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来,即使是创建了实例后修改原型。
var person = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
person.sayHi(); //"hi" (没有问题)
执行person.sayHi();时,首先在实例中查找,看有没有,然后去原型中找。因为实例与原型之间的连接只不是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回保存在那里的函数。
注意:如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的__proto__指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中指针仅指向原型,而不指向构造函数。
function Person(){
}
var person = new Person();
Person.prototype = {
name:"Nicholas",
age : 29,
job:"Software Engineer",
sayName : function(){
alert(this.name);
}
};
person.sayName(); //这里会报错
原型对象的问题
原型模式中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些基本值 的属性倒也说得过去,毕竟通过在实例上添加一同名属性,可以屏蔽原型中的对应属性。然而,包含引用类型值的属性来说,问题就比较突出了。
function Person(){
}
Person.prototype = {
name:"Nicholas",
age : 29,
job:"Software Engineer",
friends : ["Shelby","Court"], //引用类型值
sayName : function(){
alert(this.name);
}
};
var person1 = new Person();
var Person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true
由于person1和person2都是指向原型中的数组,所以值是一样的,很明显,违背了我们的初衷。于是,于是出现了组合使用构造函数模式和原型模式。
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。构造函数用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数,可谓是集两种模式之长。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends=["Shelby","Court"];
};
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
};
var person1 = new Person("Nicholas",29,"Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
这种模式是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。