深入理解 Proxy:从原理到实战应用
深入理解 Proxy:从原理到实战应用

在前端开发中,Proxy 是 JavaScript 中的一个强大特性,它可以拦截并自定义对象的基本操作(如属性访问、赋值、函数调用等)。Proxy 广泛应用于数据劫持、响应式系统、API 封装等领域,是许多现代框架(如 Vue3、MobX)的核心实现原理之一。

一、Proxy 基础概念

Proxy(代理)是 ES6 引入的一个新特性,它允许你创建一个对象的代理,从而拦截并重新定义该对象的基本操作。

基本语法

const proxy = new Proxy(target, handler);
  • target:要代理的目标对象(可以是任何类型的对象,包括数组、函数等)。
  • handler:一个对象,定义拦截行为(如 getsetapply 等)。

基本示例

const target = { name: "Alice" };
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 proxy = new Proxy(target, handler);

proxy.name; // 控制台输出: "读取属性 name"
proxy.age = 25; // 控制台输出: "设置属性 age 为 25"

在这个例子中,我们拦截了 getset 操作,并在操作发生时打印日志。

二、Proxy 的拦截操作

Proxy 的 handler 对象可以定义多种拦截方法,常见的有:

拦截方法触发时机示例
get(target, prop)读取属性时触发proxy.name
set(target, prop, value)设置属性时触发proxy.age = 25
has(target, prop)in 操作符检查属性时触发"name" in proxy
deleteProperty(target, prop)delete 操作时触发delete proxy.name
apply(target, thisArg, args)函数调用时触发proxy()
construct(target, args)new 操作时触发new ProxyClass()
ownKeys(target)Object.keys() 等操作时触发Object.keys(proxy)

拦截 getset

const user = { name: "Bob" };
const proxy = new Proxy(user, {
  get(target, prop) {
    if (prop === "fullName") {
      return `${target.name} Smith`; // 计算属性
    }
    return target[prop];
  },
  set(target, prop, value) {
    if (prop === "age" && value < 0) {
      throw new Error("年龄不能为负数");
    }
    target[prop] = value;
    return true;
  },
});

console.log(proxy.fullName); // "Bob Smith"
proxy.age = -5; // 抛出错误: "年龄不能为负数"

这里我们:

  • 拦截 get,动态计算 fullName 属性。
  • 拦截 set,对 age 进行校验。

拦截函数调用(apply

function sum(a, b) {
  return a + b;
}

const proxySum = new Proxy(sum, {
  apply(target, thisArg, args) {
    console.log(`调用函数,参数: ${args}`);
    return target(...args) * 2; // 修改返回值
  },
});

console.log(proxySum(2, 3)); // 输出: "调用函数,参数: 2,3" → 10

这里我们拦截函数调用,并修改返回值。

三、Proxy 的常见应用场景

数据响应式(如 Vue 3)

Vue 3 使用 Proxy 实现响应式数据:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, prop) {
      console.log(`读取 ${prop}`);
      return Reflect.get(target, prop);
    },
    set(target, prop, value) {
      console.log(`更新 ${prop}${value}`);
      return Reflect.set(target, prop, value);
    },
  });
}

const state = reactive({ count: 0 });
state.count++; // 输出: "读取 count" → "更新 count 为 1"

数据校验

const validator = {
  set(target, prop, value) {
    if (prop === "age" && typeof value !== "number") {
      throw new Error("年龄必须是数字");
    }
    target[prop] = value;
    return true;
  },
};

const person = new Proxy({}, validator);
person.age = "30"; // 抛出错误: "年龄必须是数字"

缓存(Memoization)

function memoize(fn) {
  const cache = new Map();
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      if (cache.has(key)) {
        console.log("缓存命中");
        return cache.get(key);
      }
      const result = target(...args);
      cache.set(key, result);
      return result;
    },
  });
}

const fibonacci = memoize((n) => (n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2)));
console.log(fibonacci(10)); // 第一次计算
console.log(fibonacci(10)); // 输出: "缓存命中"

日志记录

const withLogging = (obj) => {
  return new Proxy(obj, {
    get(target, prop) {
      console.log(`读取属性: ${prop}`);
      return target[prop];
    },
    set(target, prop, value) {
      console.log(`设置属性: ${prop} = ${value}`);
      target[prop] = value;
      return true;
    },
  });
};

const loggedObj = withLogging({});
loggedObj.name = "Alice"; // 输出: "设置属性: name = Alice"
console.log(loggedObj.name); // 输出: "读取属性: name" → "Alice"

四、Proxy 的局限性

  1. 无法拦截某些操作
    • ===instanceof 等操作无法被 Proxy 拦截。
  2. 性能开销
    • Proxy 比直接操作对象稍慢,但现代 JS 引擎已优化得很好。
  3. 目标对象必须存在
    • 不能代理原始值(如 numberstring),只能代理对象。