chap6:面向对象的程序设计
面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。前面提到过,ECMAScript 中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
ECMA-262 把对象定义为: “无序属性的集合, 其属性可以包含基本值、 对象或者函数。 ” 严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样(以及其他将要讨论的原因) ,我们可以把 ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。
6.1 理解对象
上一章曾经介绍过,创建自定义对象的最简单方式就是创建一个 Object 的实例,然后再为它添加属性和方法,如下所示。
1 var person = new Object(); 2 person.name = "Nicholas"; 3 person.age = 29; 4 person.job = "Software Engineer"; 5 person.sayName = function(){ 6 alert(this.name); 7 };
上面的例子创建了一个名为 person 的对象,并为它添加了三个属性( name 、 age 和 job )和一个方法( sayName() ) 。其中, sayName() 方法用于显示 this.name (将被解析为 person.name )的值。早期的JavaScript开发人员经常使用这个模式创建新对象。几年后,对象字面量成为创建这种对象的首选模式。前面的例子用对象字面量语法可以写成这样:
1 var person = { 2 name: "Nicholas", 3 age: 29, 4 job: "Software Engineer", 5 sayName: function(){ 6 alert(this.name); 7 } 8 };
这个例子中的 person 对象与前面例子中的person对象是一样的,都有相同的属性和方法。这些属性在创建时都带有一些特征值(characteristic) ,JavaScript通过这些特征值来定义它们的行为。
6.2 创建对象
6.2.1 工厂模式
工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程(本书后面还将讨论其他设计模式及其在 JavaScript 中的实现) 。考虑到在 ECMAScript 中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下面的例子所示。
1 function createPerson(name, age, job){ 2 var o = new Object(); 3 o.name = name; 4 o.age = age; 5 o.job = job; 6 o.sayName = function(){ 7 alert(this.name); 8 }; 9 return o; 10 } 11 var person1 = createPerson("Nicholas", 29, "Software Engineer"); 12 var person2 = createPerson("Greg", 27, "Doctor");
函数 createPerson() 能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。 可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型) 。随着 JavaScript的发展,又一个新模式出现了。
6.2.2 构造函数模式
前几章介绍过,ECMAScript 中的构造函数可用来创建特定类型的对象。像 Object 和 Array 这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如,可以使用构造函数模式将前面的例子重写如下。
1 function Person(name, age, job){ 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 this.sayName = function(){ 6 alert(this.name); 7 }; 8 } 9 var person1 = new Person("Nicholas", 29, "Software Engineer"); 10 var person2 = new Person("Greg", 27, "Doctor");
在这个例子中, Person() 函数取代了 createPerson() 函数。我们注意到, Person() 中的代码除了与 createPerson() 中相同的部分外,还存在以下不同之处:
(1)没有显式地创建对象;
(2)直接将属性和方法赋给了 this 对象;
(3)没有 return 语句。
此外,还应该注意到函数名 Person 使用的是大写字母 P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他 OO 语言,主要是为了区别于 ECMAScript 中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。
要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ;
(3) 执行构造函数中的代码(为这个新对象添加属性) ;
(4) 返回新对象。
在前面例子的最后, person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象都有一个 constructor (构造函数)属性,该属性指向 Person ,如下所示。
1 alert(person1.constructor == Person); //true 2 alert(person2.constructor == Person); //true
对象的 constructor 属性最初是用来标识对象类型的。但是,提到检测对象类型,还是 instanceof 操作符要更可靠一些。 我们在这个例子中创建的所有对象既是 Object 的实例, 同时也是 Person的实例,这一点通过 instanceof 操作符可以得到验证。
1 alert(person1 instanceof Object); //true 2 alert(person1 instanceof Person); //true 3 alert(person2 instanceof Object); //true 4 alert(person2 instanceof Person); //true
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型; 而这正是构造函数模式胜过工厂模式的地方。在这个例子中, person1 和 person2 之所以同时是 Object 的实例,是因为所有对象均继承自 Object (详细内容稍后讨论) 。
6.2.4 组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。下面的代码重写了前面的例子。
1 function Person(name, age, job){ 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 this.friends = ["Shelby", "Court"]; 6 } 7 Person.prototype = { 8 constructor : Person, 9 sayName : function(){ 10 alert(this.name); 11 } 12 } 13 var person1 = new Person("Nicholas", 29, "Software Engineer"); 14 var person2 = new Person("Greg", 27, "Doctor"); 15 person1.friends.push("Van"); 16 alert(person1.friends); //"Shelby,Count,Van" 17 alert(person2.friends); //"Shelby,Count" 18 alert(person1.friends === person2.friends); //false 19 alert(person1.sayName === person2.sayName); //true
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName() 则是在原型中定义的。而修改了 person1.friends (向其中添加一个新字符串) ,并不会影响到 person2.friends ,因为它们分别引用了不同的数组。这种构造函数与原型混成的模式,是目前在 ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。