属性描述对象的属性,被称为“元属性”,因为它可以看作是控制属性的属性。
可枚举性(enumerable)
JavaScript的最初版本,in 运算符和基于它的for/in循环,会遍历对象实例的所有属性,包括继承的属性。例如:var obj = {}; 'toString' in obj // true
在上面代码中,toString不是obj对象自身的属性,但是in运算符也返回true,导致被for/in循环遍历,这显然不太合理。后来就引入了“可枚举性”这个概念,只有可枚举的属性,才会被for/in循环遍历,同时还规定原生继承的属性都是不可枚举的,这样就保证了for/in循环的可用性。
可枚举性(enumerable)用来控制所描述的属性,是否将被包括在for/in循环之中。具体来说,如果一个属性的enumerable为false,下面三个操作不会取到该属性。
- for..in循环
- Object.keys方法
- JSON.stringify方法
【示例1】使用enumerable可以设置“秘密”属性。
var o = {a: 1, b: 2}; o.c = 3; Object.defineProperty(o, 'd', { value: 4, enumerable: false }); o.d // 4 for (var key in o) { console.log(o[key]); } // 1 // 2 // 3 Object.keys(o) // ["a", "b", "c"] JSON.stringify(o) // "{a:1, b:2, c:3}"
在上面代码中,d属性的enumerable为false,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但不是真正的私有属性,还是可以直接获取它的值。
基本上,JavaScript原生提供的属性都是不可枚举的,用户自定义的属性都是可枚举的。
与枚举性相关的几个操作的区别的是,for/in循环包括继承自原型对象的属性,Object.keys方法只返回对象本身的属性。如果需要获取对象自身的所有属性,不管是否可枚举,可以使用Object.getOwnPropertyNames方法,具体说明请参考下面内容。
【示例2】考虑到JSON.stringify方法会排除enumerable为false的值,有时可以利用这一点,为对象添加注释信息。
var car = { id: 123, color: 'red', ownerId: 12 }; var owner = { id: 12, name: 'Jack' }; Object.defineProperty(car, 'ownerInfo', { value: owner, enumerable: false }); car.ownerInfo // {id: 12, name: "Jack"} JSON.stringify(car) // "{"id": 123,"color": "red","ownerId": 12}"
在上面代码中,owner对象作为注释部分,加入car对象。由于ownerInfo属性不可枚举,所以JSON.stringify方法最后输出car对象时,会忽略ownerInfo属性。
这提示我们,如果你不愿意某些属性出现在JSON输出之中,可以把它的enumerable属性设为false。
可配置性(configurable)
可配置性(configurable)决定了是否可以修改属性描述对象。也就是说,当configurable为false的时候,value、writable、enumerable和configurable都不能被修改了。例如:
var o = Object.defineProperty({}, 'p', { value: 1, writable: false, enumerable: false, configurable: false }); Object.defineProperty(o,'p', {value: 2}) // TypeError: Cannot redefine property: p Object.defineProperty(o,'p', {writable: true}) // TypeError: Cannot redefine property: p Object.defineProperty(o,'p', {enumerable: true}) // TypeError: Cannot redefine property: p Object.defineProperties(o,'p',{configurable: true}) // TypeError: Cannot redefine property: p
上面代码首先定义对象o,并且定义o的属性p的configurable为false。然后,逐一改动value、writable、enumerable、configurable,结果都报错。
注意,writable只有在从false改为true会报错,从true改为false则是允许的。例如:
var o = Object.defineProperty({}, 'p', { writable: true, configurable: false }); Object.defineProperty(o,'p', {writable: false}) // 修改成功
至于value,只要writable和configurable有一个为true,就允许改动。例如:
var o1 = Object.defineProperty({}, 'p', { value: 1, writable: true, configurable: false }); Object.defineProperty(o1,'p', {value: 2}) // 修改成功 var o2 = Object.defineProperty({}, 'p', { value: 1, writable: false, configurable: true }); Object.defineProperty(o2,'p', {value: 2}) // 修改成功
另外,configurable为false时,直接对该属性赋值,不报错,但不会成功。例如:
var o = Object.defineProperty({}, 'p', { value: 1, configurable: false }); o.p = 2; o.p // 1
在上面代码中,o对象的p属性是不可配置的,对它赋值是不会生效的。
可配置性决定了一个变量是否可以被删除(delete)。例如:
var o = Object.defineProperties({}, { p1: { value: 1, configurable: true }, p2: { value: 2, configurable: false } }); delete o.p1 // true delete o.p2 // false o.p1 // undefined o.p2 // 2
上面代码中的对象o有两个属性,p1是可配置的,p2是不可配置的。结果,p2就无法删除。
注意,当使用var命令声明变量时,变量的configurable为false。例如:
var a1 = 1; Object.getOwnPropertyDescriptor(this,'a1') // Object { // value: 1, // writable: true, // enumerable: true, // configurable: false // }
而不使用var命令声明变量时(或者使用属性赋值的方式声明变量),变量的可配置性为true。例如:
a2 = 1; Object.getOwnPropertyDescriptor(this,'a2') // Object { // value: 1, // writable: true, // enumerable: true, // configurable: true // } // 或者写成 window.a3 = 1; Object.getOwnPropertyDescriptor(window, 'a3') // Object { // value: 1, // writable: true, // enumerable: true, // configurable: true // }
上面代码中的this.a3 = 1与a3 = 1是等价的写法。window指的是浏览器的顶层对象。
这种差异意味着,如果一个变量是使用var命令生成的,就无法用delete命令删除。也就是说,delete只能删除对象的属性。例如:
var a1 = 1; a2 = 1; delete a1 // false delete a2 // true a1 // 1 a2 // ReferenceError: a2 is not defined
可写性(writable)
可写性(writable)决定了属性的值(value)是否可以被改变。例如:
var o = {}; Object.defineProperty(o, 'a', { value: 37, writable: false }); o.a // 37 o.a = 25; o.a // 37
上面代码将o对象的a属性可写性设为false,然后改变这个属性的值,就不会有任何效果。
注意,正常模式下,对可写性为false的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使是对a属性重新赋予一个同样的值。
关于可写性,还有一种特殊情况。就是如果原型对象的某个属性的可写性为false,那么派生对象将无法自定义这个属性。例如:
var proto = Object.defineProperty({}, 'foo', { value: 'a', writable: false }); var o = Object.create(proto); o.foo = 'b'; o.foo // 'a'
在上面代码中,对象proto的foo属性不可写,结果proto的派生对象o,也不可以再自定义这个属性了。在严格模式下,这样做还会抛出一个错误。但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制,原因是这种情况下,原型链会被完全忽视。例如:
Object.defineProperty(o, 'foo', { value: 'b' }); o.foo // 'b'