对象用来存储属性。

直到现在,属性对我们来说就是简单“键:值”对的形式。但对象属性还可以更加灵活和强大。

本节我们将引入“属性描述符”的概念,介绍了“数据属性”。而在下一章中,则会介绍另一种属性——“访问器属性”。

属性标记

对象属性,除了一个 value,还有 3 个特别的属性(或叫标记,英文单词 flags)。

  • writable:如果为 true,表示属性值可修改;否则是只读的。

  • enumerable:如果为 true,表示属性可枚举,能被循环遍历(比如 for...in);否则不能被循环遍历。

  • configurable:如果为 true,表示属性能被删除(即可以这么操作 delete obj.foo),属性标记可被修改;否则既不能删除属性,也不可修改属性标记。

你可能懵逼了,心想“之前没看到过啊”。当然啦,这些可不是一般人能看到的,如果你是使用“通常的方式”创建属性的,肯定不知道原来一个属性里还有这么多东西的。当我们用上面所说的“通常的方式”创建属性的时候,这些标记的默认值都是 true,当然,这些标记值我们是可以修改的。

在此之前,先来看下如果得到这些标记。

答案是使用 Object.getOwnPropertyDescriptor 得到关于一个属性的所有信息。

此方法的语法如下:

  1. let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
  • propertyName 表示获取的是哪个属性
  • obj 是获取属性的源对象。对应到这里,我们是要获取 obj.``propertyName 的属性信息。

此方法的返回值是一个对象,被称为“属性描述符”:其中包含一个 value,还有所有标记。

举个例子:

  1. let user = {
  2. name: "John"
  3. };
  4. let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
  5. alert( JSON.stringify(descriptor, null, 2 ) );
  6. /* property descriptor:
  7. {
  8. "value": "John",
  9. "writable": true,
  10. "enumerable": true,
  11. "configurable": true
  12. }
  13. */

修改属性标记,我们使用的是 Object.defineProperty

语法如下:

  1. Object.defineProperty(obj, propertyName, descriptor)
  • objpropertyName:跟 getOwnPropertyDescriptor 方法参数意思一样。不过这里不是在获取属性,而是设置属性了。
  • descriptor:这个就是描述符对象了。因为是设置属性,那么这个描述符就是用来描述 obj.propertyName 这个属性的。

defineProperty 方法还有个特点:如果设置的属性是已经存在的,那么这个方法做的就是更新属性标记的操作;否则,就使用给定的标识符对象创建属性。另外还需要说明的是,创建属性时,没有指明的标记,默认都当做 false 设置。

举个例子。下面的 name 属性的所有标记值都会是 false

  1. let user = {};
  2. Object.defineProperty(user, "name", {
  3. value: "John"
  4. });
  5. let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
  6. alert( JSON.stringify(descriptor, null, 2 ) );
  7. /*
  8. {
  9. "value": "John",
  10. "writable": false,
  11. "enumerable": false,
  12. "configurable": false
  13. }
  14. */

与上面以“通常方式”创建的属性(user.name)相比,这里的不同之处在于:当前所有的标记值都为 false。因此,如果需要指定某个标记为 true,必须在 descriptor 中明确指明。

现在我们再来细看,这里的每个标记具体会影响属性哪些方面的表现。

不可写的

我们通过设置 user.name 属性的 writable 标记为 false,将该属性变成不可写的(non-writable,即不能赋值)。

image.png

现在 username 属性是修改不了了,除非再使用 defineProperty 方法去覆盖 writable 标志为 true

💡 提示:严格模式下会报错**
虽然赋值失败了,但上面的代码并没有报错。同样的这一段代码,如果是在严格模式下执行的,就能看见报错。

image.png

下面几乎一样的代码,不过这次变为添加新属性了。

  1. let user = { };
  2. Object.defineProperty(user, 'name', {
  3. value: 'John',
  4. // 对新属性,我们需要明确指定哪个标记为 true,
  5. // 这里没有指定 writable 标记,因此这个属性是不可写的
  6. enumerable: true,
  7. configurable: true
  8. });
  9. console.log(user.name); // "John"
  10. user.name = 'Pete';
  11. console.log(user.name); // "John"。因为 name 属性不可写,所以它的值还是 "John",而非 "Pete"

不可枚举

现在给 user 添加一个 toString 方法属性。

通常情况下,对象内置的 toString 方法是不可枚举的,不会出现在 for...in 循环里。但是如果是自己添加的 toString,就会出现在 for...in 里:

  1. let user = {
  2. name: 'John',
  3. toString () {
  4. return this.name;
  5. }
  6. };
  7. // 默认,toString 出现在枚举列表中
  8. for (let key in user) { alert(key); } // name, toString

如果我们不喜欢,就设置 enumerable: falsetoString 就不会出现在 for..in 里了:

  1. let user = {
  2. name: 'John',
  3. toString () {
  4. return this.name;
  5. }
  6. };
  7. Object.defineProperty(user, 'toString', {
  8. enumerable: false
  9. });
  10. // 现在 toString 不会出现在循环里了
  11. for (let key in user) { alert(key); } // name

Object.keys 方法也会排除不可枚举属性:

  1. alert( Object.keys(user) ); // name

不可配置

有些内置对象或属性就是不可配置的(configurable: false)。

不可配置属性不能被删除。

比如,内置属性 Math.PI 不仅是不可写的、不可枚举的,而且还不可配置。

  1. let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');
  2. alert( JSON.stringify(descriptor, null, 2) );
  3. /*
  4. {
  5. "value": 3.141592653589793,
  6. "writable": false,
  7. "enumerable": false,
  8. "configurable": false
  9. }
  10. */

所以,我们不能修改 Math.PI 的值,使用 delete Math.PI 试图删除 PI 也是无效的。

image.png

一旦将某个属性设置为不可配置后,就不能在使用 Object.defineProperty 方法修改这个属性了。

更加准确地表述是,不可配置性对 Object.defineProperty 施加了如下几个限制:

  1. 不可修改 configurable 标记。
  2. 不可修改 enumerable 标记。
  3. 不可将 writable: false 设置为 true(反之亦然)。
  4. 不可修改访问器属性的 get/set(但可以添加)。

下面我们将 user.name 定义成一个“永远封存”的常量了。

  1. let user = {};
  2. Object.defineProperty(user, 'name', {
  3. value: 'John',
  4. writable: false,
  5. configurable: false
  6. });
  7. // 我们不能修改 user.name 的值或它的标记了
  8. // 下面的操作都是无效的:
  9. // user.name = 'Pete'
  10. // delete user.name
  11. // defineProperty(user, 'name', ...)
  12. Object.defineProperty(user, 'name', { writable: true }); // 无效

💡 提示:“不可配置”不是“不可写”的意思

需要注意的是:一个不可配置但可写的属性,依然是可以被修改的。configurable: false 阻止的是属性被删除或属性标记被修改,并不影响属性可不可以被修改。

Object.defineProperties

看名字大家可能就能猜到与 Object.defineProperty 的关系了。Object.defineProperty 一次只能定义一个属性,而 Object.defineProperties(obj, descriptors) 允许一次同时定义多个属性。

语法如下:

  1. Object.defineProperties(obj, {
  2. prop1: descriptor1,
  3. prop2: descriptor2,
  4. // ...
  5. });

这里 prop1prop2 是要定义的属性,descriptor1descriptor2 则是对应属性的描述符对象。

举个例子:

  1. Object.defineProperties(obj, {
  2. name: { value: 'John', writable: false },
  3. surname: { value: 'Smith', writable: false },
  4. // ...
  5. });

如此,我们即能一次设置多个属性了。

Object.getOwnPropertyDescriptors

与上面同理,此方法与 Object.getOwnPropertyDescriptor 的不同之处在于,[Object.getOwnPropertyDescriptors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors) 方法用来一次性同时获得所有属性的描述符。

Object.defineProperties 一起使用,可以用来“克隆”对象。区别于普通的克隆方式,这可是“标志级别”的克隆方式:

  1. let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

通常克隆对象的方式如下:

  1. for (let key in user) {
  2. clone[key] = user[key]
  3. }

但它并没有复制属性标志。如果我们想要“更好”的克隆,最好使用 所以使用 Object.defineProperties 的方式。

另外一个使用 for...in 循环的缺点是会忽略 Symbol 属性,但 Object.getOwnPropertyDescriptors 方法则会返回包含 Symbol 属性在内的 所有 属性描述符。

对象级别的访问限制

属性描述符是作用在单个属性上的。

下面列出了 JavaScript 提供的、作用于整个对象级别的访问限制方法。

  1. [Object.preventExtensions(obj)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions):阻止向对象添加属性。
  2. Object.seal(obj):阻止向对象添加属性,阻止删除属性。此方法会将对象的所有属性设置成不可配置的(configurable: false)。
  3. Object.freeze(obj):阻止向对象添加属性,阻止删除属性,阻止修改属性值。此方法会将对象的所有属性设置成不可配置的(configurable: false)和不可写的(writable: false)。

同时,还提供了针对这些方法的测试方法:

  • Object.isExtensible(obj):如果被阻止添加属性就返回 false(说明对象不可扩展);否则返回 true(说明对象是可扩展的)。
  • Object.isSealed(obj):如果被阻止向对象添加属性和删除属性,并且对象当前的所有属性都是 configuable: false 的,就返回 true(说明对象是封闭了的);否则返回 false
  • Object.isFrozen(obj):如果被阻止向对象添加属性、删除属性和修改属性值,并且对象当前的所有属性都是 configurable: false, writable: false 的,就返回 true(说明对象被冻结了);否则返回 false

但这些方法实际很少使用,了解一下便可。

(完)


📄 文档信息
**
🕘 更新时间:2020/01/09
🔗 原文链接:http://javascript.info/property-descriptors