logo
返回

JS-属性描述符

对象是JavaScript的基础。

JavaScript中的一等公民函数就是对象的一个子类型,可以视为可调用的对象。 数组是对象的一种类型。 基本类型string、number、boolean都有对应的包装对象String、Number、Boolean。 除此之外,JavaScript中还有一些内置对象:Date、Error、Regexp。 ...

在JavaScript中,对象可以说是无处不在,就连JavaScript中的“继承”,本质上也是两个对象通过原型链共享信息。 接下来我们来了解一下对象的属性描述符。

属性描述符

在ES5之前,JavaScript语言本身并没有提供可以直接检测属性特性的方法,比如判断属性是否是只读。 从ES5开始,添加了对对象属性描述符的支持。现在JavaScript中支持 4 种属性描述符:

  • configurable: 当且仅当该属性的configurable键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
  • enumerable: 当且仅当该属性的enumerable键值为true时,该属性才会出现在对象的枚举属性中。
  • value: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
  • writable: 当且仅当该属性的writable键值为true时,属性的值,也就是上面的value,才能被赋值运算符改变。

在JavaScript中可以通过Object.defineProperty()来为对象设置属性描述符。

1. writable

var obj = {};
Object.defineProperty(obj,'foo', {
  value: 1,
  writable: false,
  configurable: true
});
obj; //{foo: 1}
obj.foo = 2;//严格模式会报错
obj; //{foo: 1}

通过属性描述符给对象obj定义了属性foo的value为2,所以obj.foo的值为2。writable被设置为false,我们再尝试修改对象obj.foo的时候,发现对象的值没有变化,这就是属性描述符writable:false起到了作用。

2. configurable

只要configurable为true,我们就可以用defineProperty修改属性描述符:

...
//通过defineProperty将writable改为true
Object.defineProperty(obj,'foo', {
  writable: true
});
obj.foo = 2;
obj.foo; //2, foo的值成功被修改

但是如果我们将configurable设置成false:

...
Object.defineProperty(obj,'foo', {
  writable: false
});
Object.defineProperty(obj,'foo', {
  configurable: false
});
Object.defineProperty(obj,'foo', {
  writable: true
}); //TypeError

上面代码我们先将writable设置为false,然后将configurable设置为false,然后我们再尝试将writable设置为true,这时产生了一个TypeError。 不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。 那么如果我们想将configurable重新设置为true呢?

...
Object.defineProperty(obj,'foo', {
  configurable: false
}); //TypeError

上面代码直接报错,由此可见:把configurable修改成false是单向操作,无法撤销! 注意:即便属性是configurable:false,我们还是可以把writable和enumerable的状态由true改为false,但是无法由false改为true。 configurable:false还会禁止删除这个属性:

delete obj.foo; //false  (严格模式下报错)
obj; // {foo: 2}

3. enumerable

引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉 for...in 操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的 toString 方法,以及数组的 length 属性,就通过“可枚举性”,从而避免被 for...in 遍历到。

这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说for..in循环。如果把enumerable设置成false,这个属性就不会出现在枚举中:

var obj = {foo: 1};
for(var key in obj){//进入一次循环,输出foo
 console.log(key);
}
Object.defineProperty(obj,'foo', {
  enumerable: false
});
for(var key in obj){//没有进入循环
 console.log(key);
}

如果想要查看对象某个属性的属性描述符,可以使用Object.getOwnPropertyDescriptor:

var obj = {
  foo: 1
};
Object.getOwnPropertyDescriptor(obj, 'foo');
//输出:{value: 1, writable: true, enumerable: true, configurable: true}

拓展话题:遍历

目前,有四个操作会忽略 enumerablefalse 的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。 操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。

ES6 一共有 5 种方法可以遍历对象的属性。 (1)for...in for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。 (2)Object.keys(obj) Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。 (3)Object.getOwnPropertyNames(obj) Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。 (4)Object.getOwnPropertySymbols(obj) Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。 (5)Reflect.ownKeys(obj) Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。 以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性210,其次是字符串属性ba,最后是 Symbol 属性。

不变性

有时候你会希望属性或者对象是不可改变的,在ES5中可以通过很多种方法来实现。

1. 对象常量属性

结合writable:false和configurable:false就可以创建一个真正的常量属性(不可修改、重定义或者删除):

var obj = {};
Object.defineProperty(obj,'foo', {
  value: 1,
  writable:false,
  configurable:false
});

2. 禁止扩展

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions:

var obj = {
  foo: 1
};
Object.preventExtensions(obj)
obj.bar = 2;
obj.bar; //undefined, 无法创建新的属性
Object.getOwnPropertyDescriptor(obj, 'foo');
//输出:{value: 1, writable: true, enumerable: true, configurable: true}
//说明preventExtensions不会修改属性描述符

在非严格模式下,创建属性bar会静默失败。在严格模式下,将会抛出TypeError错误。

3. 密封

使用 Object.seal 可以创建一个“密封”的对象。 实际上会在一个现有对象上调用 Object.preventExtensions 并把所有现有属性标记为 configurable:false。 密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性,但是可以修改属性值

...
Object.defineProperty(obj,'foo', {
  enumerable: false
});
Object.seal(obj);
Object.defineProperty(obj,'foo', {
  enumerable: true
});//TypeError, 无法修改属性描述符
obj.foo = 2;
obj.foo; //2, 可以修改属性值
Object.getOwnPropertyDescriptor(obj, 'foo');
//输出:{value: 2, writable: true, enumerable: true, configurable: false}
//configurable被设置为false

4. 冻结

使用Object.freeze可以创建一个冻结对象。 实际上会在一个现有对象上调用 Object.seal 并把所有“数据访问”属性标记为writable:false,这样就无法修改它们的值。

...
Object.freeze(obj);
obj.foo = 3;
obj.foo; //2, 冻结之后无法修改属性值
Object.getOwnPropertyDescriptor(obj, 'foo');
//输出:{value: 2, writable: false, enumerable: true, configurable: false}
//writable被设置为false

这个方法是可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改。

返回