JavaScript 属性定义的默认开关:值能读到,为什么遍历、改写和删除都失败
把配置对象从普通赋值改成 Object.defineProperty,只写 value,读取值一切正常。随后 Object.keys 看不到这个字段,赋新值或删除又抛出异常。问题不在对象丢失,而在新建属性时没有填写的 writable、enumerable、configurable 默认都为 false,行为与常见的普通赋值不同。
三个标志分别控制数据属性能否改值、能否参加相应枚举,以及能否删除或重新配置。它们各有独立作用,不能统称为“只读”。下面把代码保存为 demo.mjs 后运行 node demo.mjs;模块采用严格模式,因此失败赋值会以异常体现,便于明确验证。
先查看描述符,再解释表面现象
AI概念示意图:两只抽屉的三枚独立扣锁分别关闭和打开,表示三项属性标志。图片不是运行截图。
import assert from "node:assert/strict";
const locked = {};
Object.defineProperty(locked, "token", {value: 7});
assert.equal(locked.token, 7);
assert.equal(Object.hasOwn(locked, "token"), true);
assert.deepEqual(Object.getOwnPropertyDescriptor(locked, "token"), {
value: 7, writable: false, enumerable: false, configurable: false
});
assert.deepEqual(Object.keys(locked), []);
assert.equal(JSON.stringify(locked), "{}");
assert.deepEqual(Reflect.ownKeys(locked), ["token"]);
assert.throws(() => { locked.token = 8; }, TypeError);
assert.throws(() => { delete locked.token; }, TypeError);
assert.equal(Reflect.set(locked, "token", 8), false);
assert.equal(Reflect.deleteProperty(locked, "token"), false);
console.log("new property flags:", Object.getOwnPropertyDescriptor(locked, "token"));
const existing = {token: 7};
Object.defineProperty(existing, "token", {value: 8});
assert.deepEqual(Object.getOwnPropertyDescriptor(existing, "token"), {
value: 8, writable: true, enumerable: true, configurable: true
});
existing.token = 9;
assert.equal(existing.token, 9);
console.log("existing flags preserved:", Object.keys(existing));
const explicit = {};
Object.defineProperty(explicit, "token", {
value: 7, writable: true, enumerable: true, configurable: true
});
explicit.token = 8;
assert.deepEqual(Object.keys(explicit), ["token"]);
assert.equal(Reflect.deleteProperty(explicit, "token"), true);
assert.equal(Object.hasOwn(explicit, "token"), false);
console.log("explicit mutable property checks passed");第一份对象的 token 确实存在,Object.hasOwn 返回 true,直接读取也得到七;描述符却显示三个标志都是 false。Object.keys 返回空数组,序列化也得到空对象。数据没有消失,只是这些操作没有把不可枚举属性列出来,不能凭 JSON 输出为空就断言原对象没有状态。
严格模式下给 token 赋八、删除 token,都会触发 TypeError。Reflect.set 与 Reflect.deleteProperty 提供布尔结果,本例两者都返回 false。使用哪种接口取决于调用者如何处理失败,但不能在没有检查结果时把一次尝试当成成功修改。
新建与修改已有属性的省略规则不同
第二份对象用字面量建立 token,再调用 defineProperty 只改 value。此时被省略的标志保留原状,仍然是 true;代码随后还能普通赋值为九。不能把“新建时默认 false”机械解释成“每一次没有写出的标志都会变成 false”,排查时必须先确认属性此前是否存在。
第三份对象在第一次定义时显式写出三项 true,后续枚举、改写和删除都符合普通可变字段的预期。集中定义属性的辅助函数可以统一填这些值,但具体字段需要锁定时仍应明确覆盖,避免一次方便封装悄悄改变整个模型的可修改范围。
不可配置也不等于内部内容全部固定
本例锁定的是数值,因此不会牵涉嵌套对象。如果 value 是数组或对象,限制属性被重新赋值不等于递归冻结内部内容。与此同时,不可配置的数据属性仍有少量受规范限制的允许变更,例如原先 writable 为 true 时可以把它改为 false;所以描述符检查比一句“完全不能动”更准确。
这里展示新建时一次性设为不可写、不可配置的情况。一旦这样锁定,通常不能再把它恢复成普通可变属性。准备公共 API 时应先决定是否真的需要这种承诺,开发期发现字段漏设后,也不能假定再调用 defineProperty 就总能补救。
可见性不是保密边界
Reflect.ownKeys 仍能列出 token,知道名称的人也能直接读取其值。enumerable=false 适合减少常规枚举干扰,却不是安全隔离或秘密存储。若字段不应交给调用方,应该调整接口暴露方式,而不是依赖它恰好没出现在 JSON 字符串里。
回归测试应同时覆盖值、自有属性存在性、描述符、枚举结果和修改结果;还应固定严格模式条件,避免非严格环境的静默失败让测试误判。本文只演示普通数据属性,访问器的 get、set 有另一组定义规则,不应与 value、writable 任意混用。
资料核对日期:2026年10月2日(北京时间)。示例在 Node.js 24.19.0 中独立运行。


