元属性

课后整理 2020-12-20

属性描述对象的属性,被称为“元属性”,因为它可以看作是控制属性的属性。

可枚举性(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,下面三个操作不会取到该属性。

【示例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'