javascript

Proxy、Reflect 与 Symbol

By AI-Writer 18 min read

JavaScript 提供了多种元编程(Meta-programming)能力,允许代码在运行时检查和修改语言行为。ProxyReflectSymbol 是实现高级元编程的三大核心工具。

Proxy —— 对象的拦截器

Proxy 允许你创建一个对象的代理,拦截并自定义该对象的基本操作(如属性读取、赋值、函数调用等)。

基本语法

javascript
const target = { name: 'Alice', age: 28 };

const proxy = new Proxy(target, {
  // 拦截属性读取
  get(target, prop, receiver) {
    if (prop === 'age') {
      return `${target[prop]} 岁`;
    }
    return Reflect.get(target, prop, receiver);
  },

  // 拦截属性赋值
  set(target, prop, value, receiver) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('age 必须是数字');
    }
    return Reflect.set(target, prop, value, receiver);
  }
});

console.log(proxy.name); // 'Alice'
console.log(proxy.age);  // '28 岁'
proxy.age = 30;          // OK
// proxy.age = 'thirty'; // TypeError!

常用拦截器

javascript
const handler = {
  // 属性读取:obj.prop 或 obj[prop]
  get(target, prop, receiver) { /* ... */ },

  // 属性赋值:obj.prop = value
  set(target, prop, value, receiver) { /* ... */ },

  // 属性删除:delete obj.prop
  deleteProperty(target, prop) { /* ... */ },

  // in 运算符:prop in obj
  has(target, prop) { /* ... */ },

  // Object.keys / for...in
  ownKeys(target) { /* ... */ },

  // 获取属性描述符
  getOwnPropertyDescriptor(target, prop) { /* ... */ },

  // 定义属性
  defineProperty(target, prop, descriptor) { /* ... */ },

  // 函数调用:proxy(...args)
  apply(target, thisArg, args) { /* ... */ },

  // new 操作符:new proxy(...)
  construct(target, args, newTarget) { /* ... */ }
};

实战:响应式对象

javascript
function reactive(obj, onChange) {
  return new Proxy(obj, {
    set(target, prop, value, receiver) {
      const oldValue = target[prop];
      const result = Reflect.set(target, prop, value, receiver);
      if (oldValue !== value) {
        onChange(prop, value, oldValue);
      }
      return result;
    }
  });
}

const state = reactive({ count: 0 }, (key, val, old) => {
  console.log(`${key}: ${old} → ${val}`);
});

state.count = 1; // "count: 0 → 1"
state.count = 2; // "count: 1 → 2"

实战:私有属性保护

javascript
function withPrivate(obj, ...privateKeys) {
  return new Proxy(obj, {
    get(target, prop) {
      if (privateKeys.includes(prop)) {
        throw new Error(`'${prop}' 是私有属性,无法访问`);
      }
      return target[prop];
    }
  });
}

const user = withPrivate({ name: 'Alice', password: 'secret123' }, 'password');
console.log(user.name);      // 'Alice'
// console.log(user.password); // Error!

实战:函数参数校验

javascript
function validate(fn, schema) {
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      for (let i = 0; i < schema.length; i++) {
        if (typeof args[i] !== schema[i]) {
          throw new TypeError(
            `参数 ${i} 期望类型 ${schema[i]},实际得到 ${typeof args[i]}`
          );
        }
      }
      return target.apply(thisArg, args);
    }
  });
}

const add = validate((a, b) => a + b, ['number', 'number']);
console.log(add(2, 3)); // 5
// add('2', 3);         // TypeError!

Reflect —— 操作对象的统一 API

Reflect 提供了一组与 Proxy 拦截器对应的方法,用于以函数式方式执行对象操作。它的设计目标是让对象操作更加规范化。

Reflect 与直接操作对比

javascript
const obj = { x: 1 };

// 传统写法 vs Reflect 写法
obj.x = 2;                        // 赋值
Reflect.set(obj, 'x', 2);         // 等价的 Reflect 写法

'x' in obj;                       // in 运算符
Reflect.has(obj, 'x');            // 等价的 Reflect 写法

delete obj.x;                     // 删除属性
Reflect.deleteProperty(obj, 'x'); // 等价的 Reflect 写法

Object.defineProperty(obj, 'y', { value: 3 });
Reflect.defineProperty(obj, 'y', { value: 3 });

为什么使用 Reflect?

  1. 更合理的返回值
javascript
// Object.defineProperty 成功时返回对象(不太直观)
const result1 = Object.defineProperty(obj, 'a', { value: 1 });

// Reflect.defineProperty 成功时返回 boolean
const success = Reflect.defineProperty(obj, 'b', { value: 2 });
  1. 函数式调用
javascript
const methods = [Reflect.get, Reflect.set, Reflect.has];
const target = { foo: 'bar' };

// 可以像普通函数一样传递
methods[0](target, 'foo'); // 'bar'
  1. receiver 参数正确处理 this
javascript
const parent = {
  name: 'Parent',
  greet() {
    return `Hello from ${this.name}`;
  }
};

const child = Object.create(parent);
child.name = 'Child';

// 使用 Reflect.get 的 receiver 参数确保 this 指向正确
Reflect.get(parent, 'greet', child)(); // "Hello from Child"

Proxy + Reflect 最佳实践

在 Proxy handler 中使用 Reflect 是推荐模式:

javascript
const proxy = new Proxy(target, {
  get(target, prop, receiver) {
    console.log(`读取: ${String(prop)}`);
    // 默认行为委托给 Reflect
    return Reflect.get(target, prop, receiver);
  },

  set(target, prop, value, receiver) {
    console.log(`设置: ${String(prop)} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
});

Symbol —— 唯一的原始值

Symbol 是 ES6 引入的一种原始数据类型,每个 Symbol 值都是唯一的,不会与其他任何值相等。

创建 Symbol

javascript
const s1 = Symbol('description');
const s2 = Symbol('description');

console.log(s1 === s2); // false —— 每个 Symbol 都是唯一的

// Symbol.for 从全局注册表获取/创建
const a = Symbol.for('app.id');
const b = Symbol.for('app.id');
console.log(a === b); // true —— 同一个注册表键

// Symbol.keyFor 获取注册表键
console.log(Symbol.keyFor(a)); // 'app.id'

用作对象键

Symbol 作为对象键时不会被常规方法枚举到,适合创建”私有”属性:

javascript
const id = Symbol('id');
const secret = Symbol('secret');

const user = {
  name: 'Alice',
  [id]: 12345,
  [secret]: 'hidden-value'
};

// 常规枚举方法无法访问 Symbol 键
console.log(Object.keys(user));       // ['name']
console.log(Object.getOwnPropertyNames(user)); // ['name']

// 需要专门的方法获取 Symbol 键
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id), Symbol(secret)]
console.log(Reflect.ownKeys(user));   // ['name', Symbol(id), Symbol(secret)]

Well-Known Symbols

JavaScript 内置了一系列知名 Symbol(Well-Known Symbols),用于自定义语言内部行为:

javascript
// Symbol.iterator —— 定义对象的默认迭代器
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    return {
      current: this.from,
      last: this.to,
      next() {
        return this.current <= this.last
          ? { done: false, value: this.current++ }
          : { done: true };
      }
    };
  }
};

console.log([...range]); // [1, 2, 3, 4, 5]

// Symbol.toPrimitive —— 控制对象转为原始值的逻辑
const money = {
  amount: 100,
  currency: 'CNY',
  [Symbol.toPrimitive](hint) {
    if (hint === 'string') return `${this.amount} ${this.currency}`;
    if (hint === 'number') return this.amount;
    return `${this.amount}`;
  }
};

console.log(String(money));  // "100 CNY"
console.log(Number(money));  // 100
console.log(money + 50);     // "10050" 或 150,取决于默认行为

其他知名 Symbol

Symbol用途
Symbol.iterator定义默认迭代器(for…of)
Symbol.asyncIterator定义异步迭代器(for await…of)
Symbol.toPrimitive自定义原始值转换
Symbol.toStringTag自定义 Object.prototype.toString 标签
Symbol.hasInstance自定义 instanceof 行为
Symbol.isConcatSpreadable控制 concat 是否展开
Symbol.species指定派生对象的构造函数
Symbol.match / Symbol.replace / Symbol.search / Symbol.split自定义字符串方法行为

实战:自定义 toString 标签

javascript
class MyClass {
  [Symbol.toStringTag] = 'MyClass';
}

const obj = new MyClass();
console.log(Object.prototype.toString.call(obj)); // "[object MyClass]"

三者协作的完整示例

javascript
// 创建一个可观测的集合类
class ObservableArray extends Array {
  #listeners = new Set();

  constructor(...args) {
    super(...args);
    // 返回 Proxy 拦截数组操作
    return new Proxy(this, {
      set(target, prop, value, receiver) {
        const oldLen = target.length;
        const result = Reflect.set(target, prop, value, receiver);
        // 通知监听器
        receiver[Symbol.for('observable.notify')]({
          type: 'set',
          index: prop,
          value
        });
        return result;
      }
    });
  }

  subscribe(fn) {
    this.#listeners.add(fn);
    return () => this.#listeners.delete(fn);
  }

  [Symbol.for('observable.notify')](event) {
    this.#listeners.forEach(fn => fn(event));
  }

  // 自定义迭代行为
  *[Symbol.iterator]() {
    for (let i = 0; i < this.length; i++) {
      yield { index: i, value: this[i] };
    }
  }
}

const arr = new ObservableArray(1, 2, 3);
const unsubscribe = arr.subscribe(event => {
  console.log('变化:', event);
});

arr.push(4); // 触发通知

小结

特性作用典型场景
Proxy拦截对象操作响应式系统、校验、日志
Reflect规范化对象操作Proxy handler 中委托默认行为
Symbol创建唯一标识私有属性、自定义语言行为

Proxy、Reflect 和 Symbol 共同构成了 JavaScript 的元编程基础设施。掌握它们,你不仅能写出更强大的代码,还能深入理解 Vue 3 的响应式系统、迭代协议等高级机制的实现原理。

#javascript #proxy #reflect #symbol #元编程

评论

A

Written by

AI-Writer

Related Articles

javascript
#7

Promise 与 async/await

深入理解 JavaScript 异步编程核心——Promise 状态机、组合 API 与 async/await 语法糖,掌握错误处理与事件循环的协作关系。

Read More
javascript
#11

数组内置方法综合运用

深入掌握 forEach、map、filter、reduce、find、sort 等数组方法的组合技巧,避开常见性能陷阱,写出更高效的函数式代码。

Read More