C02 属性描述符与 Proxy
C02 属性描述符与 Proxy
摘要:对象属性除值之外还有四个标志:可写、可枚举、可配置与取值器,
Object.defineProperty与Object.getOwnPropertyDescriptor负责读写。Object.freeze只冻结第一层,深层不可变需要递归处理。Proxy在对象与调用方之间插入拦截层,配合Reflect保证默认行为不被破坏。本文给出描述符读写、深冻结与校验型代理三组实现。
关键词:属性描述符,不可变,Proxy,Reflect,拦截
所属:frontend/js / C-高级 | 上一篇:C01-迭代器与生成器 | 下一篇:C03-内存管理与性能分析
1 引言
Object.freeze 之后仍能修改嵌套对象的属性;给对象赋值时不报错却也没生效,因为属性是只读的;为对象加访问日志只能靠手工包一层 get/set 函数。三者都与属性描述符有关。
本文回答三个问题:属性的元信息怎么读怎么写、冻结到什么程度才够、拦截层如何不破坏原有语义。范围见表 1-1。
| 项 | 本文覆盖 | 本文不覆盖 |
|---|---|---|
| 描述符 | writable、enumerable、configurable、取值器 |
原型链上的描述符继承 |
| 不可变 | freeze、seal、深冻结 |
结构化克隆的性能 |
| 代理 | get/set/has/deleteProperty、Reflect |
代理性能开销(C03) |
2 预备知识
| 项 | 要求 | 核对方式 |
|---|---|---|
| 环境 | Node.js 20+ 或 Chrome 130+ | 支持 Proxy 与 Reflect |
| 前置 | B02 | 理解属性查找与原型链 |
| 文件 | js-practice/descriptors.js |
编码 UTF-8 |
3 原理与实现
3.1 四个标志与拦截层
每个自有属性都带四个标志,Proxy 则在读写路径上插入可编程的钩子,如图 3-1 所示。
flowchart LR
A["调用方 obj.key"] --> B["Proxy handler.get"]
B --> C["校验与记录"]
C --> D["Reflect.get(target, key)"]
D --> E["目标对象与描述符"]
E --> F["返回结果或抛错"]
G["Object.freeze"] --> H["把四个标志置为 false"]
图 3-1 代理拦截与描述符层的相对位置
图上可见:代理不改变目标对象,只改变访问路径;
Reflect提供与默认行为一致的函数,避免手写实现偏离规范。
3.2 完整实现
// js-practice/descriptors.js;运行环境:Node.js 20+
// 1 读取描述符
const user = { name: 'Lin' };
Object.defineProperty(user, 'id', {
value: 7,
writable: false,
enumerable: false,
configurable: false,
});
console.log(Object.getOwnPropertyDescriptor(user, 'id'));
// { value:7, writable:false, enumerable:false, configurable:false }
console.log(Object.keys(user)); // ['name'],不可枚举的不出现
try {
user.id = 8; // 严格模式下抛 TypeError
} catch (err) {
console.log(err.name); // TypeError
}
// 2 取值器:计算属性
const rect = {
w: 3,
h: 4,
get area() {
return this.w * this.h;
},
};
console.log(rect.area); // 12
// 3 深冻结:freeze 只处理第一层
function deepFreeze(obj) {
for (const key of Object.getOwnPropertyNames(obj)) {
const value = obj[key];
if (value && typeof value === 'object') deepFreeze(value);
}
return Object.freeze(obj);
}
const config = deepFreeze({ api: { base: '/v1' } });
config.api.base = '/v2'; // 静默失败;严格模式抛错
console.log(config.api.base); // /v1
// 4 校验型代理:拒绝非法写入
function createValidated(initial) {
const target = { ...initial };
const rules = {
age: (v) => Number.isInteger(v) && v >= 0 && v <= 150,
};
return new Proxy(target, {
set(obj, key, value) {
const rule = rules[key];
if (rule && !rule(value)) {
throw new RangeError(`${key} 取值非法:${value}`);
}
return Reflect.set(obj, key, value);
},
get(obj, key, receiver) {
if (typeof key === 'string' && key.startsWith('_')) {
throw new Error(`禁止访问内部字段 ${key}`);
}
return Reflect.get(obj, key, receiver);
},
});
}
const person = createValidated({ name: 'Lin', age: 30 });
person.age = 31;
console.log(person.age); // 31
try {
person.age = -1;
} catch (err) {
console.log(err.name, err.message); // RangeError age 取值非法:-1
}
// 5 只读视图:写入一律拒绝
const readonly = (obj) =>
new Proxy(obj, {
set() {
throw new Error('只读对象不可修改');
},
deleteProperty() {
throw new Error('只读对象不可删除');
},
});
const frozen = readonly({ level: 1 });
try {
frozen.level = 2;
} catch (err) {
console.log(err.message); // 只读对象不可修改
}
console.log(Object.isFrozen(frozen)); // false,代理本身未冻结
描述符标志的含义见表 3-1。
| 标志 | 含义 | 置为 false 的后果 |
|---|---|---|
writable |
值可否被赋值修改 | 赋值静默失败或抛错 |
enumerable |
可否出现在 for...in、Object.keys |
遍历时被跳过 |
configurable |
可否删除或重新定义 | 不可删除,也不可改标志 |
get/set |
取值器与赋值器 | 与 value 互斥 |
不可变手段的强度见表 3-2。
| 方法 | 阻止新增 | 阻止删除 | 阻止修改 | 深度 |
|---|---|---|---|---|
preventExtensions |
是 | 否 | 否 | 仅自身 |
seal |
是 | 是 | 否 | 仅自身 |
freeze |
是 | 是 | 是 | 仅自身 |
递归 freeze |
是 | 是 | 是 | 全部层级 |
只读 Proxy |
是 | 是 | 是 | 访问层 |
4 验证与讨论
- 执行脚本,确认不可枚举属性不出现在
Object.keys中,且赋值抛TypeError。 - 把
deepFreeze换成Object.freeze,确认config.api.base被改为/v2。 - 给代理写入
age: 200,确认抛出RangeError并说明原因。 - 对
frozen执行delete frozen.level,确认抛出只读错误。
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| 不可枚举 | 不出现在遍历结果中 | enumerable: false |
| 只读属性 | 赋值抛 TypeError |
严格模式下的行为 |
| 深冻结 | 嵌套层同样只读 | 递归遍历 |
| 代理校验 | 非法值被拒绝并给出原因 | set 钩子 |
讨论与局限。 Object.isFrozen 对代理返回 false,因为代理本身没有冻结标志,判定需检查目标对象。另,代理会拖慢热路径上的属性访问,且在 for...in、JSON.stringify 等场景下行为与普通对象存在细微差异。
5 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
freeze 后仍能改嵌套属性 |
只冻结第一层 | 递归冻结或用只读代理 |
| 赋值没生效也不报错 | 非严格模式下静默失败 | 加 'use strict' 或模块脚本 |
代理后 this 指向异常 |
get 未传 receiver |
用 Reflect.get(obj, key, receiver) |
代理对象 instanceof 失败 |
原型未转发 | getPrototypeOf 钩子转给目标 |
JSON.stringify 结果缺字段 |
相关属性不可枚举 | 检查 enumerable 标志 |
6 结论
| 序号 | 结论 | 依据 |
|---|---|---|
| 1 | 属性除值之外还有四个标志 | 表 3-1 |
| 2 | freeze 只处理一层,深层需递归 |
表 3-2 |
| 3 | 代理钩子内用 Reflect 保持默认行为 |
图 3-1 |
| 4 | 代理不改变目标对象,isFrozen 需查目标 |
4 节讨论 |
- 上一篇:C01-迭代器与生成器
- 下一篇:C03-内存管理与性能分析
- 返回:C00-总结 | 科目 README
参考文献
[1] MDN Web Docs · Proxy,https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy(访问于 2026-10-31)
[2] ECMA-262 · 10.1 Ordinary Object Internal Methods,https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots(访问于 2026-10-31)

评论 0
还没有评论,快来抢沙发吧~