Skip to content

属性描述符与不可变性:configurablewritableenumerable——如何真正“冻结”一个对象?

“对象的不可变性,不是由值决定的,而是由属性的内部描述符共同协议的结果。”

当你写下:

js
const obj = { x: 1 };

你可能以为 obj.x 是一个简单的键值对。

但真相是:
每个属性背后,都隐藏着一个属性描述符(Property Descriptor)——
它是一个由 writableenumerableconfigurablevalue 构成的元数据结构,
决定了该属性能否被修改、枚举、删除,甚至能否被重新定义。

我们来从 属性描述符的类型、三元属性的语义、Object.defineProperty 的底层机制、深度冻结的实现原理 四个维度,彻底拆解:

一、属性描述符的两种类型:数据属性与访问器属性

来自 ECMA-262 §6.1.7

JavaScript 中每个属性都有一个属性描述符,它是一个记录(Record),包含以下字段:

1. 数据属性(Data Property)

字段含义默认值
[[Value]]属性的值undefined
[[Writable]]能否被赋值运算符改变false(严格模式下)
[[Enumerable]]能否被 for...in 枚举false
[[Configurable]]能否被 delete 删除,或被 defineProperty 重新配置false
js
Object.defineProperty(obj, 'x', {
  value: 1,
  writable: true,
  enumerable: true,
  configurable: true
});

2. 访问器属性(Accessor Property)

字段含义默认值
[[Get]]获取属性时调用的函数undefined
[[Set]]设置属性时调用的函数undefined
[[Enumerable]]同上false
[[Configurable]]同上false
js
Object.defineProperty(obj, 'y', {
  get() { return this._y * 2; },
  set(val) { this._y = val; },
  enumerable: true,
  configurable: true
});

数据属性和访问器属性互斥——一个属性不能同时有 valueget/set

二、writableenumerableconfigurable 的真实语义

writable:决定属性能否被赋值

js
const obj = {};
Object.defineProperty(obj, 'x', {
  value: 1,
  writable: false,
  configurable: true
});

obj.x = 2; // 无效(非严格模式)
// 严格模式下:TypeError

writable: false 意味着 [[Set]] 内部方法会拒绝赋值。

enumerable:决定属性能否被 for...inObject.keys 枚举

js
const obj = { a: 1 };
Object.defineProperty(obj, 'b', {
  value: 2,
  enumerable: false
});

console.log(Object.keys(obj)); // ['a']
for (let k in obj) console.log(k); // 只输出 'a'

JSON.stringify 也只序列化可枚举属性。

configurable:决定属性能否被删除或重新配置

js
const obj = {};
Object.defineProperty(obj, 'x', {
  value: 1,
  configurable: false
});

delete obj.x; // 无效(非严格模式)
Object.defineProperty(obj, 'x', { value: 2 }); // TypeError

一旦 configurable: false,就不能再变回 true,这是单向锁定。

三、Object.defineProperty[[DefineOwnProperty]] 协议

来自 ECMA-262 §10.1.6

当你调用:

js
Object.defineProperty(obj, 'x', descriptor)

引擎会执行 [[DefineOwnProperty]] 内部方法,
并根据目标属性的当前描述符,进行一系列状态迁移检查

关键规则:

  1. 如果目标属性 [[Configurable]] === false

    • 不能改变 [[Configurable]][[Enumerable]]
    • 不能从数据属性转为访问器属性(反之亦然)
    • 只能修改 [[Value]][[Writable]](且不能从 truefalse
  2. 如果 [[Configurable]] === true

    • 可以任意修改描述符

configurable: false 是属性的“最终状态”,一旦设置,描述符的变更路径被永久限制。

四、如何真正“冻结”一个对象?

JavaScript 提供了多个层级的不可变性控制:

1. Object.preventExtensions(obj)

禁止添加新属性

js
Object.preventExtensions(obj);
obj.newProp = 1; // 无效(非严格模式)

2. Object.seal(obj)

preventExtensions + 所有属性 configurable: false

js
Object.seal(obj);
delete obj.x; // 无效
Object.defineProperty(obj, 'x', { value: 2 }); // TypeError

3. Object.freeze(obj)

seal + 所有属性 writable: false

js
Object.freeze(obj);
obj.x = 2; // 无效
delete obj.x; // 无效

五、Object.freeze 的实现原理

js
function myFreeze(obj) {
  if (obj == null) return obj;

  // 1. 防止扩展
  Object.preventExtensions(obj);

  // 2. 遍历所有自有属性(包括不可枚举)
  const keys = Reflect.ownKeys(obj);
  for (let key of keys) {
    const desc = Object.getOwnPropertyDescriptor(obj, key);

    // 3. 如果是数据属性,设置 writable: false
    if ('value' in desc) {
      desc.writable = false;
    }

    // 4. 无论何种属性,设置 configurable: false
    desc.configurable = false;

    // 5. 重新定义属性
    Object.defineProperty(obj, key, desc);
  }

  return obj;
}

这就是 Object.freeze 的核心逻辑。

六、Object.freeze 是浅冻结

js
const obj = { nested: { x: 1 } };
Object.freeze(obj);

obj.nested.x = 2; // 成功!
obj.newProp = 1;   // 失败

因为 freeze 只冻结对象自身的属性,
obj.nested[[Value]] 是一个对象引用,
其内部状态仍可变。

深冻结实现

js
function deepFreeze(obj) {
  if (obj === null || typeof obj !== 'object') return obj;

  // 先冻结自身
  Object.freeze(obj);

  // 再递归冻结所有对象类型的属性
  const keys = Reflect.ownKeys(obj);
  for (let key of keys) {
    const value = obj[key];
    if (value && (typeof value === 'object' || typeof value === 'function')) {
      deepFreeze(value);
    }
  }

  return obj;
}

注意:需处理循环引用(可用 WeakSet 缓存已冻结对象)。

七、不可变性的边界:[[Prototype]] 也能被修改?

js
const obj = {};
Object.defineProperty(obj, '__proto__', {
  value: { x: 1 },
  writable: false,
  configurable: false,
  enumerable: false
});

虽然 __proto__ 是遗留属性,但规范允许通过 defineProperty 修改 [[Prototype]](使用 Object.setPrototypeOf 更标准)。

Object.freeze 不会冻结 [[Prototype]] 的可变性。

一句话总结

对象的不可变性,是由属性描述符 writableenumerableconfigurable 共同决定的协议状态。

  • writable: false 禁止赋值
  • enumerable: false 禁止枚举
  • configurable: false 禁止删除和重新配置
  • Object.freeze 通过将所有自有属性设为 writable: falseconfigurable: false,实现浅冻结
  • 真正的“完全冻结”需要递归处理嵌套对象

结语:理解属性描述符,就是理解“对象的元控制”

大多数人认为 const obj = { x: 1 } 是“不可变的”,
而你理解的是:

const 只冻结绑定,不冻结对象内部状态;真正的不可变性必须通过 [[Writable]][[Configurable]] 的元数据控制。”

你不再只是“定义对象”,
而是在编程对象的元行为协议