支持面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念。通过实例化类可以创建出任意多个具有相同属性和方法的对象。而ECMAScript中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
ECMA-262把对象定义为:“无序属性的集合,其值可以包括基本值,对象或函数”。因此,我们可以将对象想象成一个散列表:包含多个无序的名值对,名代表属性名或方法名,值可以是数据或函数。
1. 对象创建
ECMAScript中创建自定义对象的方式主要有四种,如下:
- 创建Object实例
- 字面量
- 工厂模式
- 构造函数(原型)
1.1 通过Object实例创建自定义对象
通过Object实例创建自定义对象的方式在前文中已经多次提及,一个例子如下:
var person = new Object();
person.name = "Ivan";
person.age = 22;
person.job = "Software Engineer";
person.sayName = function(){
alert(this.name);
}
上面的例子首先创建了一个名为Object对象,然后为其添加了三个属性和一个方法。
1.2 通过字面量方式创建对象
下面是一个通过对象字面量创建对象的例子:
var person = {
name: "Ivan",
age: 22,
job: "Software Engineer",
sayName: function(){
alert(this.name);
}
}
上面的例子通过定义名值对的方式创建了一个拥有三个属性和一个方法的person对象。
ECMAScript中的对象存在两种类型的属性:数据属性和访问器属性。
1.2.1 数据属性
数据属性拥有4个描述其行为的特性:
- [[Configurable]]:表示能否通过delete删除这个属性,能否修改属性的特性,或者能否将属性修改为访问器属性。默认值为true。
- [[Enumerable]]:表示能否通过for-in语句循环返回该属性。默认值为true。
- [[Writable]]:表示能否修改属性的值。默认值为true。
- [[Value]]:包含这个属性的值。读取属性时,从这个位置读;写入属性值的时候,把新值写入这个位置。默认值为undefined。
上面的定义的特性是为了实现JavaScript引擎而用的,JavaScript不能直接访问他们。因此要修改数据属性默认的特性,必须使用ECMAScript的Object.defineProperty()方法。这个方法接收三个参数:属性所在对象,属性名和一个描述符对象。例如:
var person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Ivan"
});
alert(person.name); //"Ivan"
person.name = "Roy";
alert(person.name); //"Ivan"
上面的例子将name属性设置为不可修改,在非严格模式下,尝试修改的操作会被忽略;而在严格模式下,修改操作会直接抛出错误。
1.2.2 访问器属性
访问器属性不包含数据值:它包含一对getter和setter函数(非必需)。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;而在写入属性值时,会调用setter函数,这个函数负责如何处理数据。访问器属性也有四个描述其行为的特性:
- [[Configurable]]:表示能否通过delete删除这个属性,能否修改属性的特性,或者能否将属性修改为数据属性。默认值为true。
- [[Enumerable]]:表示能否通过for-in语句循环返回该属性。默认值为true。
- [[Get]]:在读取属性值时调用的函数。默认值为undefined。
- [[Set]]:在写入属性值时调用的函数。默认值为undefined。
访问器属性也不能直接定义,必须通过Object.defineProperty()方法来定义。下面是一个例子:
var book = {
__year: 2004,
edition: 1
};
Object.defineProperty(book, "year", {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
alert(book.edition); //2
使用 ECMAScript 5的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述 符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果 是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set;如果是数据属性,这 个对象的属性有 configurable、enumerable、writable 和 value。例如:
var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); alert(descriptor.value); //2004
alert(descriptor.configurable); //false
1.3 使用工厂模式创建对象
虽然使用Object构造函数或对象字面量都能够创建一个对象,但这两种方式有一个明显的缺陷:使用同一个接口创建很多对象,会产生大量的重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。下面是一个例子:
function createPerson(name, age, job){
var person = new Object();
o.name = name;
o.age =age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return person;
}
上面的createPerson()函数能够根据接收的参数来构建一个包含必要信息的Person对象。这解决了上面提到的创建大量同种引用类型的对象带来的重复代码的问题。然而这种方式创建出来的对象无法确定对象的实际类型。因此JavaScript引入了通过构造函数创建的自定义对象的模式。
1.4 自定义构造函数
前面提到,ECMAScript可以通过构造函数来创建特定类型的对象。例如前文中的Object()构造函数,这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以通过创建自定义的构造函数,从而创建自定义对象。下面是一个例子:
function Person(name, age, job){
this.name = name;
this.age =age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person = new Person("Ivan", 22, "Software Engineer");
person.sayName(); //"Ivan"
上面定义了一个名为Person的函数,通常我们约定将构造函数的首字母大写。构造函数本质上与普通函数并没有什么区别(可以像调用普通函数一样调用构造函数,但是这样无法创建出一个对象实例),只是通过new关键字能够创建出一个对象实例而已。new操作符后跟构造函数实质上ECMAScript会在后台做以下几件事:
(1)创建一个新对象
(2)将构造函数的作用域赋给新对象(因此this指向了这个新对象)
(3)执行构造函数中的代码(为新对象添加属性)
(4)返回这个新对象
创建自定义的构造函数意味着可以将它的实例标记为一种特定类型,而这正是构造函数胜过工厂模式的地方。通过instanceof操作符,我们可以确定某个对象是否是特定引用类型的实例。例如:
alert(person instanceof Object); //true
alert(person instanceof Person); //true
由于所有的引用类型均继承自Object类型,因此这儿person instanceof Object也返回true。
构造模式虽然好用,但却有一个最主要的问题:就是每个方法在每个实例上都会被创建一遍。我们前面说到,函数的本质是对象,因此这种方式创建的每一个对象都包含一个名为sayName的Function实例。这就造成了内存浪费。一种解决方案是,将函数的的定义转移到构造函数外部:
function Person(name, age, job){
this.name = name;
this.age =age;
this.job = job;
this.sayName = sayName;
}
sayName = function(){
alert(this.name);
}
var person = new Person("Ivan", 22, "Software Engineer");
person.sayName(); //"Ivan"
上面的例子将Person对象的sayName方法指向了一个全局函数,因此所有的Person实例都共享这个Function对象。然而将大量的属于特定引用类型的方法,放在全局环境中定义,一方面这个方法从语义上来说不应当是全局方法,另一方面这样大大破坏了这个引用类型的封装性。解决方案是使用原型模式。