在JavaScript中,属性决定了一个对象的状态,本文详细的研究了它们是如何工作的.
属性类型
JavaScript中有三种不同类型的属性:命名数据属性(named data properties),命名访问器属性(named accessor properties)以及内部属性(internal properties).
命名数据属性
这种属性就是我们通常所用的"普通"属性,它用来将一个字符串名称映射到某个值上.比如,下面的对象obj有一个名为字符串"prop"的数据属性,该属性的值为数字123.
var obj = { prop: 123 };
你可以获取(读取)到一个属性的值:
console.log(obj.prop); // 123 console.log(obj["prop"]); // 123
你还可以设置(写入)一个属性的值:
obj.prop = "abc";
obj["prop"] = "abc";
命名访问器属性
另外,还可以借助函数来获取或设置一个属性的值.这些函数称之为访问器函数(accessor function).控制属性读取的访问器函数称之为getter.控制属性写入的访问器函数称之为setter.
var obj = { get prop() { return "Getter"; }, set prop(value) { console.log("Setter: "+value); } }
让我们操作一下obj的属性:
> obj.prop 'Getter' > obj.prop = 123; Setter: 123
内部属性
有一些属性仅仅是为规范所用的,称之为内部属性,因为它们无法通过JavaScript直接访问到,但是它们的确存在,并且影响着程序的表现.内部属性的名称比较特殊,它们都被两个中括号包围着.下面有两个例子:
- 内部属性[[Prototype]]指向了所属对象的原型.该属性的值可以通过Object.getPrototypeOf()函数读取到.该属性的值只能在创建一个新对象的时候通过Object.create()或者__proto__来设置 [1].
- 内部属性[[Extensible]]决定了是否能给所属对象添加新的属性.该属性的值可以通过Object.isExtensible()读取到.还可以通过Object.preventExtensions()将该属性的值设置为false.一旦设置为false,就无法再设置回true了.
属性特性
一个属性的所有状态,包括它的数据和元数据,都存储在该属性的特性(attributes)中.属性拥有自己的特性,就像对象拥有自己的属性一样.特性的名称经常写成类似内部属性的形式(双中括号).
下面是命名数据属性拥有的特性:
- [[Value]] 存储着属性的值,也就是属性的数据.
- [[Writable]] 存储着一个布尔值,表明该属性的值是否可以改变.
下面是命名访问器属性拥有的特性:
- [[Get]] 存储着getter,也就是在读取这个属性时调用的函数.该函数返回的值也就是这个属性的值.
- [[Set]] 存储着setter,也就是在为这个属性赋值时调用的函数.该函数在调用时会被传入一个参数,参数的值为所赋的那个新值.
下面是两种类型的属性都有的特性:
- [[Enumerable]] 存储着一个布尔值.可以让一个属性不能被枚举,在某些操作下隐藏掉自己(下面会有详细讲解).
- [[Configurable]] 存储着一个布尔值.如果为false,则你不能删除这个属性,不能改变这个属性的大部分特性(除了[[Value]]),不能将一个数据属性重定义成访问器属性,或者反之.换句话说就是:[[Configurable]]控制了一个属性的元数据的可写性.
默认值
如果你不明确的指定某个特性的值,则它们会被赋一个默认值:
特性名称 | 默认值 |
[[Value]] | undefined |
[[Get]] | undefined |
[[Set]] | undefined |
[[Writable]] | false |
[[Enumerable]] | false |
[[Configurable]] | false |
这些默认值对于属性描述符尤其重要.
属性描述符
属性描述符(property descriptor)可以将一个属性的所有特性编码成一个对象并返回.该对象的每个属性都对应着所属属性的一个特性.例如,下面是一个值为123的只读属性的属性描述符:
{ value: 123, writable: false, enumerable: true, configurable: false }
你也可以使用一个访问器属性来实现上面这个拥有只读特性的数据属性,其属性描述符如下:
{ get: function () { return 123 }, //没有set,也就是只读 enumerable: true, configurable: false }
使用属性描述符的函数
使用属性描述符的函数
在使用下面的函数时会用到属性描述符:
- Object.defineProperty(obj, propName, propDesc)
创建或改变对象obj的propName属性,propName属性的特性通过属性描述符propDesc给出.返回修改后的obj对象.例如:
var obj = Object.defineProperty({}, "foo", { value: 123, enumerable: true // writable和configurable为默认值 });
- Object.defineProperties(obj, propDescObj)
Object.defineProperty()的批处理版本.对象propDescObj的每个属性都指定了要给原对象obj添加或修改的一个属性和对应的属性描述符.例如:
var obj = Object.definePropertys({}, { foo: { value: 123, enumerable: true }, bar: { value: "abc", enumerable: true } });
- Object.create(proto, propDescObj?)
首先,创建一个原型为proto的对象.然后,如果提供了可选参数propDescObj,则会按照Object.defineProperties添加属性的方式给这个新对象添加属性.最后,返回操作后的新对象.例如,下面的代码创建的对象和上面的Object.definePropertys例子创建的对象完全一样:
var obj = Object.create(Object.prototype, { foo: { value: 123, enumerable: true }, bar: { value: "abc", enumerable: true } });
- Object.getOwnPropertyDescriptor(obj, propName)
返回对象obj的名为propName的自身属性(非继承来的)的属性描述符.如果没有这个自身属性,则返回undefined.
> Object.getOwnPropertyDescriptor(Object.prototype, "toString") { value: [Function: toString], writable: true, enumerable: false, configurable: true } > Object.getOwnPropertyDescriptor({}, "toString") undefined
可枚举性
本节会解释什么操作会受到属性的可枚举性的影响,什么操作不会.我们首先假设已经定义了如下这样的对象proto和obj:
var proto = Object.defineProperties({}, { foo: { value: 1, enumerable: true }, bar: { value: 2, enumerable: false } }); var obj = Object.create(proto, { baz: { value: 1, enumerable: true }, qux: { value: 2, enumerable: false } });
需要注意的是,所有对象(包括上面的proto)通常来说都至少有一个原型Object.prototype [2]:
> Object.getPrototypeOf({}) === Object.prototype true
我们常用的内置方法比如toString和hasOwnPropertyare等实际上都是定义在Object.prototype身上的.
受可枚举性影响的操作
可枚举性只影响两种操作:for-in循环和Object.keys().
for-in循环会遍历到一个对象的所有可枚举属性的名称,包括继承来的属性:
> for (var x in obj) console.log(x); //没有遍历到Object.prototype上不可枚举的属性qux baz foo
Object.keys()返回一个对象的所有可枚举的自身属性(非继承的)的名称组成的数组:
> Object.keys(obj)
[ 'baz' ]
如果你想获取到所有的自身属性,则应该使用Object.getOwnPropertyNames().
不受可枚举性影响的操作
除了上面的两个操作,其他的操作都会忽略掉属性的可枚举性.一些读取操作会使用到继承来的属性:
> "toString" in obj true > obj.toString [Function: toString]
还有一些操作只会考虑自身属性:
> Object.getOwnPropertyNames(obj) [ 'baz', 'qux' ] > obj.hasOwnProperty("qux") true > obj.hasOwnProperty("toString") false > Object.getOwnPropertyDescriptor(obj, "qux") { value: 2, writable: false, enumerable: false, configurable: false } > Object.getOwnPropertyDescriptor(obj, "toString") undefined
创建,删除,定义属性的操作只会影响到自身属性:
obj.propName = value obj["propName"] = value delete obj.propName delete obj["propName"] Object.defineProperty(obj, propName, desc) Object.defineProperties(obj, descObj)
最佳实践
一般的规则是:系统创建的属性是不可枚举的,用户创建的属性是可枚举的:
> Object.keys([]) [] > Object.getOwnPropertyNames([]) [ 'length' ] > Object.keys(['a']) [ '0' ]
特别是针对原型对象上的方法来说:
> Object.keys(Object.prototype) [] > Object.getOwnPropertyNames(Object.prototype) [ hasOwnProperty', 'valueOf', 'constructor', 'toLocaleString', 'isPrototypeOf', 'propertyIsEnumerable', 'toString' ]
因此,在你自己写的代码中,通常不应该给内置的原型对象添加属性,如果你必须要这么做,则应该把这个属性设置为不可枚举的,以防止影响到其他代码.
正如我们所看到的,不可枚举的好处是:能确保已有的代码中的for-in语句不受到从原型继承来的属性的影响.但是,不可枚举的属性只能够创建一种"for-in只会遍历一个对象的自身属性"这样的幻觉.在你的代码中,仍应该尽可能避免使用for-in[3].
如果你把对象当成是字符串到值的Map来使用的话,则你应该只操作自身属性且要忽略掉可枚举性.不过这种情况下还有很多其他陷阱需要考虑[4].
结论
在本文中,我们对属性的性质(称之为特性)进行了研究.需要注意的是,实际上JavaScript引擎并不是必须得通过特性来组织一个属性,它们主要是作为ECMAScript规范中定义的一个抽象操作.但有时候这些特性也会明确的出现在语言代码中,比如在属性描述符中.
更进一步的知识(2ality):
- 阅读 “JavaScript properties: inheritance and enumerability” 了解更多的受继承和枚举性影响的属性相关操作的知识.
- 阅读 “JavaScript inheritance by example” 进一步了解JavaScript的继承.
参考
- JavaScript: __proto__
- What object is not an instance of Object?
- Iterating over arrays and objects in JavaScript
- The pitfalls of using objects as maps in JavaScript