面向对象(Object-Oriented,OO) 的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。ECMAScript中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。 ECMAScript-262把对象定义为:”无序属性的集合,其属性可以包含基本值,对象或者函数。”严格地讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样,我们可以把ECMAScript的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。 每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员定义的类型。
创建对象 工厂模式 工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装特定接口创建对象的细节。
1
2
3
4
5
6
7
8
9
10
11
12
13
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" );
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数模式 ECMAScript中的构造函数可用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如,可以使用构造函数模式将前面的例子重写如下。
1
2
3
4
5
6
7
8
9
10
11
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()中的代码除了与createPerson()中相同的部分外,还存在以下不同之处:
没有显示地创建对象;
直接将属性和方法赋给了this对象;
没有return语句。
此外,还应该注意到函数名Person使用的是大写字母P。这个做法借鉴自其它OO语言,主要是为了区别于ECMAScript中的其他函数;因为构造函数也是函数,只不过可以用来创建对象而已。 以这种方式调用构造函数实际上会经历以下4个步骤:
创建一个对象;
将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
执行构造函数中的代码(为这个新对象添加属性);
返回新对象。
这个例子中创建的所有对象既是Object的实例,同时也是Person的实例,这一点通过instanceof操作符可以得到验证。
1
2
3
4
alert(person1 instanceof Object); //true
alert(person2 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Person); //true
创建自定义的构造函数意味着将来可以将它的实际标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。
构造函数的问题 使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。从逻辑角度讲,此时的构造函数也可以这样定义。
1
2
3
4
5
6
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)" ); //与声明函数在逻辑上是等价的
}
从这个角度来看构造函数,更容易明白Person实例都包含一个不同的Function实例(以显示name属性)的本质。说明白些,以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的,一下代码可以证明这一点。
1
alert(person1.sayName == person2.sayName); //false
然而,创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
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就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person (){}
Person.prototype.name = "Nicholas" ;
Person.prototype.age = 29;
Person.prototype = "Software Engineer" ;
Person.prototype = function () {
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
原型与in操作符 同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。
1
2
3
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
要取得对象上所有可枚举的实例属性,可以使用ECMAScript5的Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person () {
}
Person.prototype.name = "Nicholas" ;
Person.prototype.age = "29" ;
Person.prototype.job = "Software Engineer" ;
Person.prototype.sayName = function () {
alert(this.name);
}
var keys = Object.keys(Person.prototype);
alert(keys) //"name,age,job,sayName"
var p1 = new Person();
p1.name = "Rob" ;
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"
如果是通过Person的实例调用,则Object.keys()返回的数组只包含”name”和”age”这两个实例属性。 如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。
1
2
var keys = Object.getOwnPropertyNames();
alert(keys); //"constructor,name,age,job,sayName"
注意结果中包含了不可枚举的constructor属性。Object.keys()和Object.getOwnPropertyNames()方法都可以用代替for-in循环。
更简单的原型语法 前面例子中美添加一个属性和方法都要敲一遍 Person.prototype。 为了减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是一个包含所有属性和方法的对象字面量来重写整个原型对象。
1
2
3
4
5
6
7
8
function.prototyype = {
name : "Nicholas" ,
age : 29,
job : "Software Engineer" ,
sayName : function () {
alert(this.name);
}
};
上面的代码中将Person.prototype设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外:constructor 属性不再指向 person 了。因为没创建一个函数,就会同时创建它的prototype对象,因此 constructor 属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向 Person 函数。此时,尽管 instanceof 操作符还能返回正确的结果,但通过 constructor 已经无法确定对象的类型了。
1
2
3
4
5
6
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true
如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。
1
2
3
4
5
6
7
8
9
10
11
12
function Person () {
}
Person.prototype = {
constructor : Person,
name : "Nicholas" ,
age : 29,
job : "Software Engineer" ,
sayName : function () {
alert(this.name);
}
};
原型的动态性 由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能立即从实例上反映出来----即使是先创建了实例后修改原型也照样如此。
1
2
3
4
5
6
7
var friend = new Person();
Person.prototype.sayHi = function () {
alert("hi" );
};
friend.sayHi(); //"hi" (没有问题!)
其原因可以归结为实例与原型之间的松散连接关系。当我们调用 friend.sayHi() 时,首先会在实例中搜索名为 sayHi() 的属性,在没有找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi 属性并返回保存在那里的函数。 尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function () {
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas" ,
age : 29,
job : "Software Engineer" ,
sayName : function () {
alert(this.name);
}
};
原型对象的问题 原型模式省略了为构造函数传递初始化参数这一节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来不便,但还不是原型的问题。原型模式的最大问题是由其共享的本质所导致的。 原型中所有的属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型的属性来说,问题就比较突出了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person () {
}
Person.prototype = {
constructor: Person,
name : "Nicholas" ,
age : 29,
job : "Software Engineer" ,
firends : ["Sheby" , "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
实例一般都是要有属于自己的全部属性的。这个问题正是我们很少看到有人单独使用原型模式的原因所在。
组合使用构造函数模式和原型模式 创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的脚本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
动态原型模式 有其它OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正是致力解决这个问题的方案,它把所有的信息都封装在了构造函数中,而通过在狗仔函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
if (typeof this.sayName != "function") {
person.prototype.sayName = function(){
alert(this.name);
}
};
}
var friend = new Person("Nicholas", 29, "Software Enginner");
friend.sayName();
这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。其中, if 语句检查的可以是初始化之后应该存在的任何属性或方法---不必用一大堆 if 语句检查每个属性和方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用 instanceof 操作符确定它的类型。
使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断实例与新原型之间的联系。
寄生构造函数模式 1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function (){
alert(this.name);
};
reuturn o;
}
var firend = new Person("Nicholas" , 29, "Software Enginner" );
除了使用new操作符并把使用的包装函数叫做构造函数外,这个模式跟工厂模式其实一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。 这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SpecialArray (){
//创建数组
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipdString = function (){
return this.join("|" );
}
//返回数组
return values;
}
var colors = new SpecialArray("red" , "blue" , "green" );
alert(colors.toPipedString()); //"red" |"blue" |"green"
需要说明的是,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与构造函数外部创建的函数没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,在可以使用其他模式的情况下,不要使用这种模式。
稳妥构造函数模式 所谓稳妥对象,指的是没有公共属性,而且方法也不引用this的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function (){
alert(name);
};
//返回对象
return o;
}
var friend = Person("Nicholas" , 29, "Software Enginner" );
friend.sayName(); //"Nicholas"
稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境
与寄生构造函数模式类型,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此 instanceof 操作符对这种对象也没有意义。