C02 属性描述符与 Proxy

2026-10-31 17:080 阅读约 13 分钟JavaScript

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 验证与讨论

  1. 执行脚本,确认不可枚举属性不出现在 Object.keys 中,且赋值抛 TypeError。
  2. 把 deepFreeze 换成 Object.freeze,确认 config.api.base 被改为 /v2。
  3. 给代理写入 age: 200,确认抛出 RangeError 并说明原因。
  4. 对 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 节讨论

参考文献

[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

0/2000

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