对象是面向对象语言的一个最基本的概念。ECMA-262 对于对象的定义是:
对象是无序属性的集合,其属性可以包含基本值、对象或者函数。
每个对象都是基于一个引用类型创建的,可以是原生类型(如 Array, String, Function, Data 等),也可以是开发人员自己定义的类型。
以下定义一个 person 对象,并为它添加属性和方法。
var person = new Object();
person.name = 'Nightn';
person.age = 24;
person.school = 'ZJU';
person.work = function() {
console.log(this.name + ' is doing what he likes!');
}
我们更常用的对象创建方法是用对象字面量:
var person = {
name: 'Nightn',
age: 24,
school: 'ZJU',
sayName: function() {
console.log(this.name + ' is doing what he likes!');
}
}
要深入理解对象,首先要深入理解对象组成的基本要素——对象的属性。对象的每一个属性在创建的时候就带有了一些特征,我们称之为属性的特性(attribute),ECMA-262 规定的特性描述了属性的各种特征,这些特性主要是为了实现 JavaScript 引擎用的,在 JavaScript 不能直接访问,为了表示它,我们使用双方括号表示法(如 [[Enumerable]])。
ECMAScript 中有两种属性:数据属性和访问器属性。
我认为,数据属性和访问器属性可以用一句话来概括:前者存放数据,后者封装数据。
数据属性包含了一个数据值的位置。通过这个位置可以读取和写入数据。数据属性有 4 个描述其行为的特性。
-
[[Configurable]] :表示属性是否可配置。可配置具体包括:是否可以通过 delete 删除属性、是否可以将该数据属性转换为访问器属性、是否能修改属性的特性 。对于常规定义的属性,它们的 [[Configurable]] 特性的默认值是 true,但对于用
Object.defineProperty
方法(后面会讲到)定义的属性,[[Configurable]] 特性的默认值是 false。是否能修改属性的特性,这个说法不准确。准确的说法是:是否可以修改属性的 [[Configurable]] 和 [[Enumerable]] 特性。[[Writable]] 和 [[Value]] 特性是否可以修改,与 [[Configurable]] 的值无关。
-
[[Enumerable]] :表示属性是否可枚举。即是否能够通过 for in 枚举出来。对于常规定义的属性,它们的 [[Enumerable]] 特性的默认值是 true,但对于用
Object.defineProperty
方法定义的属性,[[Enumerable]] 特性的默认值是 false。 -
[[Writable]] :表示属性是否可修改。对于常规定义的属性,它们的 [[Writable]] 特性的默认值是 true,但对于用
Object.defineProperty
方法定义的属性,[[Writable]] 特性的默认值是 false。 -
[[Value]] :包含这个属性的数据值。属性值的读取和写入都是通过这个特性完成的。默认值为 undefined 。
我们可以通过 Object.defineProperty()
方法进行属性的高级定义。它接收 3 个参数:目标属性所在的对象、目标属性、属性描述对象。参数具体解释无需赘言,直接看例子:
var person = new Object();
Object.defineProperty(person, 'name', {
configurable: true,
enumerable: true,
writable: true,
value: 'nightn'
});
// 输出试试
console.log(person.name); // nightn
for (var prop in person) {
console.log(prop); // name 说明属性的 [[Enumerable]] 特性为 true
}
person.name = 'nice';
console.log(person.name) // nice 说明属性的 [[Writable]] 为 true,而且我们将 [[Value]] 特性的值从 'nightn' 改为了 'nice'
delete person.name;
console.log(person.name); // undefined 说明 [[Configurable]] 为 true
再举一个例子。我们知道,检测数组可以使用 Array.isArray()
方法(如果你想深入了解数组,可以看看 JS 数组)。我们之所以可以访问构造函数 Array 的 isArray 方法,就是因为构造函数实际上也是对象,isArray 只是这个对象的一个属性。那么我们使用 for in 去遍历 Array 属性的时候,会发现什么都看不到。
console.log('isArray' in Array); // true 说明 isArray 的确是 Array 的属性
// 但是通过 for in 没有办法看到 isArray
for (var prop in Array) {
console.log(prop);
}
根据之前讲的特性,我们很容易推测出:isArray
属性的 [[Enumerable]] 特性是 false,所以我们没办法枚举出来。我们可以做一个测试:
// 将 Array 对象的 isArray 属性的 [[Enumerable]] 特性设置为 true
Object.defineProperty(Array, 'isArray', {
enumerable: true
});
// 我们再用 for in 测试一下
for (var prop in Array) {
console.log(prop); // 成功打印了 isArray
}
以上测试结果还说明了一个事实:我们可以配置 isArray 的特性,那意味着 isArray 的 [[Configurable]] 特性的值是 true。最后我们再测试一下 [[Writable]] 特性。
Array.isArray = 1;
Array.isArray([]); // TypeError: Array.isArray is not a function
以上结果说明了 isArray 的 [[Writable]] 特性也是 true。
通过以上方法,我们的确可以逐步的获取一个属性的所有特性值,可是每次都这样操作太麻烦了,而且还会更改属性(或者说,不能实现属性的无损检测,哈哈)。幸运的是,ES5 提供了一个读取属性特性的方法:
Object.getOwnPropertyDescriptor()
这个方法具体描述我不啰嗦了,直接看例子吧:
// 记得把之前修改的 isArray 还原,如果你在 devtool 运行,直接刷新浏览器就行
var desc = Object.getOwnPropertyDescriptor(Array, 'isArray');
console.log(desc.configurable); // true
console.log(desc.enumerable); // false
console.log(desc.writable); // true
console.log(desc.value); // ƒ isArray() { [native code] }
// 以上输出也验证了我们之前对 isArray 的测试结果
访问器属性不像数据属性那样包含着数据,它包含的是一对 getter、setter 函数(不过,这两个函数都不是必须的)。就像我之前所说的,数据属性存放数据、访问器属性封装数据。封装指的是数据的读写不再是那么轻而易举了,访问器属性加了限制:在读取访问器属性时,会调用 getter 函数;在写入访问器属性时,会调用 setter 函数。有了这个限制,我们就能在读写属性时做一点额外的工作(比如写入时对传入值进行验证,或者联动地修改其他值)。访问器属性有如下 4 个特性:
- [[Configurable]] : 表示属性是否可以配置。包括是否能用 delete 删除该属性、是否可以将属性由访问器属性转换为数据属性、是否能修改属性的特性。
- [[Enumerable]] :表示属性是否可以枚举,即是否可以通过 for in 枚举出来。
- [[Get]] : 在读取属性时调用的函数。默认为 undefined。
- [[Set]] : 在写入属性时调用的函数。默认为 undefined。
举个栗子(栗子来源于《JavaScript高级程序设计》):
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 = 2000;
console.log(book.year); // 2004 访问器属性的 setter 加了限制,修改失败
book.year = 2005
console.log(book.edition); // 2 联动修改其他属性
在以上例子中,我们定义了一个访问器属性 year
,我们用这个访问器属性封装了私有的数据属性 _year
。这样相当于在读写 _year
的时候进行了限制。访问器属性最常用的场合就是设置一个属性时,一并设置它的联动属性。
JS 中使用访问器属性封装私有的数据属性,和 C# 中用属性封装私有字段非常相似,有兴趣的同学可以看看关于 C# 属性的介绍。
最后再介绍一个一次性定义多个属性的方法:
Object.defineProperties()
Talking is cheap, so I just show you code:
var book = {
edition: 1
};
// 使用 Object.defineProperties 方法一次定义多个属性
Object.defineProperties(book, {
// 这是一个数据属性
_year: {
value: 2004
},
// 这是一个访问器属性
year:{
get: function() {
return this._year;
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
未完待续……
- 《JavaScript高级程序设计》(第 3 版) 6.1