属性描述符与不可变性:configurable、writable、enumerable——如何真正“冻结”一个对象?
“对象的不可变性,不是由值决定的,而是由属性的内部描述符共同协议的结果。”
当你写下:
const obj = { x: 1 };你可能以为 obj.x 是一个简单的键值对。
但真相是:
每个属性背后,都隐藏着一个属性描述符(Property Descriptor)——
它是一个由 writable、enumerable、configurable 和 value 构成的元数据结构,
决定了该属性能否被修改、枚举、删除,甚至能否被重新定义。
我们来从 属性描述符的类型、三元属性的语义、Object.defineProperty 的底层机制、深度冻结的实现原理 四个维度,彻底拆解:
一、属性描述符的两种类型:数据属性与访问器属性
来自 ECMA-262 §6.1.7:
JavaScript 中每个属性都有一个属性描述符,它是一个记录(Record),包含以下字段:
1. 数据属性(Data Property)
| 字段 | 含义 | 默认值 |
|---|---|---|
[[Value]] | 属性的值 | undefined |
[[Writable]] | 能否被赋值运算符改变 | false(严格模式下) |
[[Enumerable]] | 能否被 for...in 枚举 | false |
[[Configurable]] | 能否被 delete 删除,或被 defineProperty 重新配置 | false |
Object.defineProperty(obj, 'x', {
value: 1,
writable: true,
enumerable: true,
configurable: true
});2. 访问器属性(Accessor Property)
| 字段 | 含义 | 默认值 |
|---|---|---|
[[Get]] | 获取属性时调用的函数 | undefined |
[[Set]] | 设置属性时调用的函数 | undefined |
[[Enumerable]] | 同上 | false |
[[Configurable]] | 同上 | false |
Object.defineProperty(obj, 'y', {
get() { return this._y * 2; },
set(val) { this._y = val; },
enumerable: true,
configurable: true
});数据属性和访问器属性互斥——一个属性不能同时有 value 和 get/set。
二、writable、enumerable、configurable 的真实语义
writable:决定属性能否被赋值
const obj = {};
Object.defineProperty(obj, 'x', {
value: 1,
writable: false,
configurable: true
});
obj.x = 2; // 无效(非严格模式)
// 严格模式下:TypeErrorwritable: false 意味着 [[Set]] 内部方法会拒绝赋值。
enumerable:决定属性能否被 for...in 和 Object.keys 枚举
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:决定属性能否被删除或重新配置
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:
当你调用:
Object.defineProperty(obj, 'x', descriptor)引擎会执行 [[DefineOwnProperty]] 内部方法,
并根据目标属性的当前描述符,进行一系列状态迁移检查。
关键规则:
如果目标属性
[[Configurable]] === false:- 不能改变
[[Configurable]]和[[Enumerable]] - 不能从数据属性转为访问器属性(反之亦然)
- 只能修改
[[Value]]或[[Writable]](且不能从true变false)
- 不能改变
如果
[[Configurable]] === true:- 可以任意修改描述符
configurable: false 是属性的“最终状态”,一旦设置,描述符的变更路径被永久限制。
四、如何真正“冻结”一个对象?
JavaScript 提供了多个层级的不可变性控制:
1. Object.preventExtensions(obj)
禁止添加新属性
Object.preventExtensions(obj);
obj.newProp = 1; // 无效(非严格模式)2. Object.seal(obj)
preventExtensions + 所有属性 configurable: false
Object.seal(obj);
delete obj.x; // 无效
Object.defineProperty(obj, 'x', { value: 2 }); // TypeError3. Object.freeze(obj)
seal + 所有属性 writable: false
Object.freeze(obj);
obj.x = 2; // 无效
delete obj.x; // 无效五、Object.freeze 的实现原理
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 是浅冻结
const obj = { nested: { x: 1 } };
Object.freeze(obj);
obj.nested.x = 2; // 成功!
obj.newProp = 1; // 失败因为 freeze 只冻结对象自身的属性,obj.nested 的 [[Value]] 是一个对象引用,
其内部状态仍可变。
深冻结实现
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]] 也能被修改?
const obj = {};
Object.defineProperty(obj, '__proto__', {
value: { x: 1 },
writable: false,
configurable: false,
enumerable: false
});虽然 __proto__ 是遗留属性,但规范允许通过 defineProperty 修改 [[Prototype]](使用 Object.setPrototypeOf 更标准)。
而 Object.freeze 不会冻结 [[Prototype]] 的可变性。
一句话总结
对象的不可变性,是由属性描述符 writable、enumerable、configurable 共同决定的协议状态。
writable: false禁止赋值enumerable: false禁止枚举configurable: false禁止删除和重新配置Object.freeze通过将所有自有属性设为writable: false和configurable: false,实现浅冻结- 真正的“完全冻结”需要递归处理嵌套对象
结语:理解属性描述符,就是理解“对象的元控制”
大多数人认为 const obj = { x: 1 } 是“不可变的”,
而你理解的是:
“const 只冻结绑定,不冻结对象内部状态;真正的不可变性必须通过 [[Writable]] 和 [[Configurable]] 的元数据控制。”
你不再只是“定义对象”,
而是在编程对象的元行为协议。