Published on

对象属性描述符

在 JavaScript 中,对象的属性并不仅仅是一个简单的键值对。每个属性的背后,都由一组内部的特性 (attributes) 所定义,这些特性共同构成了一个属性描述符 (Property Descriptor)。这个描述符精确地控制了该属性的行为,例如它是否可写、可枚举或可配置。理解并掌握通过 Object.defineProperty 等元编程 (Metaprogramming) API 来操作这些底层特性,是实现诸如“只读属性”、数据劫持(如 Vue 2 的响应式原理)和创建不可变对象等高级功能的关键。

属性描述符的类型与特性

属性描述符主要分为两种相互排斥的类型。

数据描述符 vs. 存取描述符

  • 数据描述符 (Data Descriptor): 拥有一个具体的数据值。其特性包含 value, writable, enumerable, configurable
  • 存取描述符 (Accessor Descriptor): 由一对 getter-setter 函数来定义。其特性包含 get, set, enumerable, configurable

一个属性描述符不能同时拥有 valuewritablegetset

属性特性 (Property Attributes)

对于通过常规方式(如 const obj = { a: 1 };)创建的属性,其特性默认为 true。这些特性也被称为“属性标志”。

  • writable: 一个布尔值。当为 true 时,该属性的 value 可以被修改。当为 false 时,该属性变为只读。
  • enumerable: 一个布尔值。当为 true 时,该属性会出现在对象的枚举中(例如,在 for...in 循环或 Object.keys() 的结果中)。
  • configurable: 一个布尔值。当为 true 时,该属性的描述符可以被再次修改,并且该属性可以从其所属对象中被删除。当为 false 时,该属性将不可删除,且除了 valuewritable 之外的所有特性都不可再被修改。

核心 API:definePropertygetOwnPropertyDescriptor

Object.getOwnPropertyDescriptor()

此方法用于查询一个对象自有属性(非继承属性)的完整属性描述符。

const user = { name: "Alice" };

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

console.log(descriptor);
// 输出:
// {
//   value: "Alice",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

Object.defineProperty()

此方法用于在一个对象上定义一个新属性,或修改一个现有属性的特性。

  • 修改现有属性: defineProperty 会更新指定属性的标志。
  • 定义新属性: 如果属性不存在,defineProperty 会使用给定的值和标志创建一个新属性。
defineProperty 的默认行为

当使用 defineProperty 定义一个新属性时,如果没有在描述符中显式提供 writable, enumerable, configurable 标志,它们的默认值将全部为 false。这与通过对象字面量创建属性的行为截然不同。

defineProperty 实践

const user = {};

// 使用 defineProperty 创建一个新属性
Object.defineProperty(user, 'id', {
  value: '123',
  writable: false, // 设置为只读
  enumerable: true,
  // configurable 未提供,默认为 false
});

// 查询描述符
console.log(Object.getOwnPropertyDescriptor(user, 'id'));
// { value: '123', writable: false, enumerable: true, configurable: false }

// 尝试修改只读属性 (在非严格模式下静默失败,在严格模式下抛出 TypeError)
user.id = '456';
console.log(user.id); // "123"

// 尝试删除不可配置的属性 (静默失败或抛出 TypeError)
delete user.id;
console.log(user.id); // "123"

对象的整体限制 (Object-level Restrictions)

除了对单个属性进行精细控制外,JavaScript 还提供了一系列 API,用于对整个对象的扩展性和属性的可配置性施加限制。这些限制是不可逆的。

  • Object.preventExtensions(obj)
    • 作用: 禁止向对象添加新的属性。
    • 检测: Object.isExtensible(obj) 返回 false
  • Object.seal(obj)
    • 作用: 禁止添加新属性,也禁止删除现有属性。它会将所有现有属性的 configurable 标志设置为 false
    • 检测: Object.isSealed(obj) 返回 true
  • Object.freeze(obj)
    • 作用: 这是最严格的限制级别。它禁止添加、删除、修改任何属性。它会将所有现有属性的 configurablewritable 标志都设置为 false
    • 检测: Object.isFrozen(obj) 返回 true