JavaScript Proxy 和 Reflect

254 阅读3分钟

一、Proxy 基本概念

1. 什么是 Proxy

Proxy 是 ES6 引入的元编程特性,用于创建一个对象的代理,从而可以拦截和自定义对象的基本操作。

const target = {};
const handler = {
  get(target, property) {
    return `拦截读取: ${property}`;
  }
};
const proxy = new Proxy(target, handler);

console.log(proxy.message); // "拦截读取: message"

2. 核心术语

  • target:被代理的目标对象
  • handler:包含拦截器(trap)的对象
  • trap:拦截目标对象操作的函数

二、Proxy 拦截操作

1. 常用拦截器

属性访问拦截

const handler = {
  get(target, prop) {
    console.log(`读取属性 ${prop}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`设置属性 ${prop} = ${value}`);
    target[prop] = value;
    return true; // 表示设置成功
  }
};

方法调用拦截

const handler = {
  apply(target, thisArg, argumentsList) {
    console.log(`调用函数,参数: ${argumentsList}`);
    return target.apply(thisArg, argumentsList);
  }
};

function sum(a, b) { return a + b; }
const proxySum = new Proxy(sum, handler);
proxySum(1, 2); // 输出日志后返回3

2. 完整拦截器列表

拦截器触发时机示例
get读取属性proxy.x
set设置属性proxy.x = 1
hasin 操作符'x' in proxy
deletePropertydelete 操作delete proxy.x
apply函数调用proxy()
constructnew 操作new proxy()
ownKeysObject.keys()等Object.keys(proxy)
getPrototypeOfObject.getPrototypeOf()Object.getPrototypeOf(proxy)
setPrototypeOfObject.setPrototypeOf()Object.setPrototypeOf(proxy, proto)
isExtensibleObject.isExtensible()Object.isExtensible(proxy)
preventExtensionsObject.preventExtensions()Object.preventExtensions(proxy)
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor(proxy, 'x')
definePropertyObject.defineProperty()Object.defineProperty(proxy, 'x', desc)

三、Proxy 高级应用

1. 数据验证

const validator = {
  set(target, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value) || value < 0) {
        throw new TypeError('年龄必须是正整数');
      }
    }
    target[prop] = value;
    return true;
  }
};

const person = new Proxy({}, validator);
person.age = 30; // 正常
person.age = 'young'; // 抛出错误

2. 自动填充对象

const autoFiller = {
  get(target, prop) {
    if (!(prop in target)) {
      target[prop] = {};
    }
    return target[prop];
  }
};

const tree = new Proxy({}, autoFiller);
tree.branch1.branch2.leaf = 'value';
console.log(tree); // { branch1: { branch2: { leaf: 'value' } } }

3. 负数组索引

const negativeArray = arr => new Proxy(arr, {
  get(target, prop, receiver) {
    const index = parseInt(prop);
    if (index < 0) {
      prop = target.length + index;
    }
    return Reflect.get(target, prop, receiver);
  }
});

const arr = negativeArray(['a', 'b', 'c']);
console.log(arr[-1]); // "c"

四、Reflect 基本概念

1. 什么是 Reflect

Reflect 是一个内置对象,提供拦截 JavaScript 操作的方法,这些方法与 Proxy 的拦截器一一对应。

const obj = { x: 1 };
console.log(Reflect.get(obj, 'x')); // 1

2. Reflect 的设计目的

  1. 统一操作 API:将语言内部操作(如 [[Get]][[Set]])暴露为函数
  2. 与 Proxy 配合:每个 Proxy 拦截器都有对应的 Reflect 方法
  3. 替代部分 Object 方法:更合理的返回值设计

五、Reflect 主要方法

1. 基本操作方法

方法等价操作区别
Reflect.get(target, prop)target[prop]支持 receiver
Reflect.set(target, prop, value)target[prop] = value返回布尔值
Reflect.has(target, prop)prop in target-
Reflect.deleteProperty(target, prop)delete target[prop]返回布尔值
Reflect.ownKeys(target)Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))-

2. 对象扩展方法

方法等价操作区别
Reflect.defineProperty(target, prop, desc)Object.defineProperty(target, prop, desc)返回布尔值
Reflect.getOwnPropertyDescriptor(target, prop)Object.getOwnPropertyDescriptor(target, prop)-
Reflect.isExtensible(target)Object.isExtensible(target)-
Reflect.preventExtensions(target)Object.preventExtensions(target)返回布尔值

3. 原型相关方法

const obj = {};
const proto = { x: 1 };

// 设置原型
Reflect.setPrototypeOf(obj, proto);
console.log(obj.x); // 1

// 获取原型
console.log(Reflect.getPrototypeOf(obj) === proto); // true

4. 函数调用方法

function greet(name) {
  return `Hello, ${name}`;
}

// 函数调用
console.log(Reflect.apply(greet, null, ['Alice'])); // "Hello, Alice"

// 构造函数调用
class Person {}
const p = Reflect.construct(Person, []);
console.log(p instanceof Person); // true

六、Proxy 和 Reflect 配合使用

1. 默认转发模式

const proxy = new Proxy(target, {
  get(target, prop, receiver) {
    console.log(`GET ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`SET ${prop}=${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
});

2. 实现观察者模式

function createObservable(target, observer) {
  return new Proxy(target, {
    set(target, prop, value, receiver) {
      const oldValue = target[prop];
      const result = Reflect.set(target, prop, value, receiver);
      if (result && oldValue !== value) {
        observer(prop, oldValue, value);
      }
      return result;
    }
  });
}

const data = createObservable({}, (prop, oldVal, newVal) => {
  console.log(`属性 ${prop}${oldVal} 变为 ${newVal}`);
});

data.x = 1; // 输出: "属性 x 从 undefined 变为 1"
data.x = 2; // 输出: "属性 x 从 1 变为 2"

七、实际应用场景

1. API 客户端封装

function createAPI(baseUrl) {
  return new Proxy({}, {
    get(target, endpoint) {
      return async function(params) {
        const url = `${baseUrl}/${endpoint}`;
        const response = await fetch(url, {
          method: 'POST',
          body: JSON.stringify(params)
        });
        return response.json();
      };
    }
  });
}

const api = createAPI('https://api.example.com');
const userData = await api.users({ id: 123 });

2. 不可变数据

function createImmutable(obj) {
  return new Proxy(obj, {
    set() {
      throw new Error('不可修改');
    },
    deleteProperty() {
      throw new Error('不可删除');
    },
    defineProperty() {
      throw new Error('不可定义新属性');
    }
  });
}

const immutable = createImmutable({ x: 1 });
immutable.x = 2; // 抛出错误

3. 自动缓存

function createCached(fn) {
  const cache = new Map();
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      if (cache.has(key)) {
        return cache.get(key);
      }
      const result = Reflect.apply(target, thisArg, args);
      cache.set(key, result);
      return result;
    }
  });
}

const heavyCompute = createCached(x => {
  console.log('计算中...');
  return x * x;
});

heavyCompute(2); // 输出"计算中..."后返回4
heavyCompute(2); // 直接返回4,不输出

八、注意事项与最佳实践

1. Proxy 限制

  1. 目标对象不变:Proxy 不会修改目标对象本身
  2. 严格相等proxy === target 为 false
  3. 内部插槽:某些内置对象(如 Date、Map)的内部插槽无法被拦截

2. 性能考虑

  1. 代理有开销:简单操作用原生方式更快
  2. 避免多层代理:代理的代理会显著降低性能
  3. 热路径优化:关键性能路径避免使用代理

3. 最佳实践

  1. 透明代理:尽量保持代理行为与目标对象一致
  2. 合理使用 Reflect:在拦截器中使用对应 Reflect 方法
  3. 明确文档:记录代理的自定义行为
  4. 错误处理:在拦截器中妥善处理异常

九、浏览器兼容性

1. 支持情况

  • Proxy:主流浏览器及 Node.js 6+
  • Reflect:主流浏览器及 Node.js 6+

2. 不支持的解决方案

// 简单的Proxy polyfill检查
if (typeof Proxy === 'undefined') {
  console.warn('Proxy not supported, falling back to direct access');
  Proxy = function(target) { return target; };
}

十、总结

Proxy 和 Reflect 为 JavaScript 提供了强大的元编程能力:

  1. Proxy

    • 创建对象的代理,拦截基本操作
    • 实现数据绑定、验证、观察等高级功能
    • 自定义对象基本行为
  2. Reflect

    • 提供操作对象的统一API
    • 与Proxy拦截器一一对应
    • 改进Object方法的返回值设计

两者结合使用可以实现许多传统JavaScript难以实现的高级模式,如:

  • 透明的数据观察
  • 自动化的资源管理
  • 动态接口适配
  • 领域特定语言(DSL)

这些特性在现代框架和库中有广泛应用,如Vue 3的响应式系统就基于Proxy实现。合理使用Proxy和Reflect可以显著提升代码的灵活性和可维护性。