2022 年 10 月 18 日

属性标志和描述符

众所周知,对象可以存储属性。

到目前为止,对我们来说,属性只是一对简单的“键值”对。但实际上,对象属性是一件更灵活、更强大的事情。

在本章中,我们将学习其他配置选项,在下一章中,我们将看到如何将它们无形地转换为 getter/setter 函数。

属性标志

对象属性除了value之外,还有三个特殊属性(所谓的“标志”)

  • writable – 如果为 true,则可以更改值,否则为只读。
  • enumerable – 如果为 true,则在循环中列出,否则不列出。
  • configurable – 如果为 true,则可以删除属性并修改这些属性,否则不能。

我们还没有看到它们,因为通常它们不会显示。当我们“以通常的方式”创建属性时,所有这些属性都是 true。但我们也可以随时更改它们。

首先,让我们看看如何获取这些标志。

方法 Object.getOwnPropertyDescriptor 允许查询属性的完整信息。

语法是

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
obj
要从中获取信息的的对象。
propertyName
属性的名称。

返回的值是一个所谓的“属性描述符”对象:它包含值和所有标记。

例如

let user = {
  name: "John"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/

要更改标记,我们可以使用 Object.defineProperty

语法是

Object.defineProperty(obj, propertyName, descriptor)
objpropertyName
要应用描述符的对象及其属性。
descriptor
要应用的属性描述符对象。

如果属性存在,defineProperty 会更新其标记。否则,它会使用给定的值和标记创建属性;在这种情况下,如果未提供标记,则假定为 false

例如,此处创建了一个具有所有假标记的属性 name

let user = {};

Object.defineProperty(user, "name", {
  value: "John"
});

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": "John",
  "writable": false,
  "enumerable": false,
  "configurable": false
}
 */

将其与上面“正常创建的”user.name 进行比较:现在所有标记都是假的。如果这不是我们想要的,那么我们最好在 descriptor 中将它们设置为 true

现在让我们通过示例了解标记的效果。

不可写

让我们通过更改 writable 标记使 user.name 不可写(无法重新分配)

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  writable: false
});

user.name = "Pete"; // Error: Cannot assign to read only property 'name'

现在,除非其他人应用自己的 defineProperty 来覆盖我们的,否则没有人可以更改我们用户的名称。

错误仅在严格模式下出现

在非严格模式下,在写入不可写属性等时不会发生错误。但操作仍然不会成功。在非严格模式下,违反标记的操作会被静默忽略。

以下为相同的示例,但属性是从头开始创建的

let user = { };

Object.defineProperty(user, "name", {
  value: "John",
  // for new properties we need to explicitly list what's true
  enumerable: true,
  configurable: true
});

alert(user.name); // John
user.name = "Pete"; // Error

不可枚举

现在让我们向 user 添加一个自定义的 toString

通常情况下,对象的内置 toString 是不可枚举的,它不会显示在 for..in 中。但如果我们添加自己的 toString,那么默认情况下它会显示在 for..in 中,如下所示

let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

// By default, both our properties are listed:
for (let key in user) alert(key); // name, toString

如果我们不喜欢,那么我们可以设置 enumerable:false。然后它将不会出现在 for..in 循环中,就像内置循环一样

let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

Object.defineProperty(user, "toString", {
  enumerable: false
});

// Now our toString disappears:
for (let key in user) alert(key); // name

不可枚举的属性也会从 Object.keys 中排除

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

不可配置

不可配置标记 (configurable:false) 有时会对内置对象和属性进行预设。

不可配置属性无法删除,其属性无法修改。

例如,Math.PI 是不可写、不可枚举和不可配置的

let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": 3.141592653589793,
  "writable": false,
  "enumerable": false,
  "configurable": false
}
*/

因此,程序员无法更改 Math.PI 的值或覆盖它。

Math.PI = 3; // Error, because it has writable: false

// delete Math.PI won't work either

我们也不能将 Math.PI 再次更改为 writable

// Error, because of configurable: false
Object.defineProperty(Math, "PI", { writable: true });

我们绝对无法使用 Math.PI 做任何事。

将属性设为不可配置是单行道。我们无法使用 defineProperty 将其改回。

请注意:configurable: false 阻止更改属性标志及其删除,同时允许更改其值。

此处 user.name 不可配置,但我们仍然可以更改它(因为它可写)

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  configurable: false
});

user.name = "Pete"; // works fine
delete user.name; // Error

此处我们让 user.name 成为“永久密封”的常量,就像内置的 Math.PI

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  writable: false,
  configurable: false
});

// won't be able to change user.name or its flags
// all this won't work:
user.name = "Pete";
delete user.name;
Object.defineProperty(user, "name", { value: "Pete" });
唯一可能的属性更改:可写 true → false

关于更改标志有一个小例外。

我们可以将不可配置属性的 writable: true 更改为 false,从而阻止其值修改(以增加另一层保护)。但不能反过来。

Object.defineProperties

有一个方法 Object.defineProperties(obj, descriptors) 允许一次定义多个属性。

语法是

Object.defineProperties(obj, {
  prop1: descriptor1,
  prop2: descriptor2
  // ...
});

例如

Object.defineProperties(user, {
  name: { value: "John", writable: false },
  surname: { value: "Smith", writable: false },
  // ...
});

因此,我们可以一次设置多个属性。

Object.getOwnPropertyDescriptors

要一次获取所有属性描述符,我们可以使用 Object.getOwnPropertyDescriptors(obj) 方法。

它可以与 Object.defineProperties 一起用作一种“标志感知”的克隆对象的方式

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

通常,当我们克隆一个对象时,我们会使用赋值来复制属性,如下所示

for (let key in user) {
  clone[key] = user[key]
}

…但这样不会复制标志。因此,如果我们想要一个“更好”的克隆,则首选 Object.defineProperties

另一个区别是 for..in 忽略符号和不可枚举属性,但 Object.getOwnPropertyDescriptors 返回所有属性描述符,包括符号和不可枚举的属性。

全局密封对象

属性描述符在单个属性级别上起作用。

还有一些方法可以限制对整个对象的访问

Object.preventExtensions(obj)
禁止向对象添加新属性。
Object.seal(obj)
禁止添加/删除属性。为所有现有属性设置 configurable: false
Object.freeze(obj)
禁止添加/删除/更改属性。为所有现有属性设置 configurable: false, writable: false

还有针对它们的测试

Object.isExtensible(obj)
如果禁止添加属性,则返回 false,否则返回 true
Object.isSealed(obj)
如果禁止添加/删除属性,并且所有现有属性都有 configurable: false,则返回 true
Object.isFrozen(obj)
如果禁止添加/删除/更改属性,并且所有当前属性均为 configurable: false, writable: false,则返回 true

这些方法在实践中很少使用。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题或提交 Pull 请求,而不是评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入一些代码,请使用 <code> 标签,对于多行 - 将它们包装在 <pre> 标签中,对于 10 行以上 - 使用沙盒(plnkrjsbincodepen…)