用"中间人"思维理解 ES6 Proxy:从生活场景到代码实现

你有没有过这样的经历?租房时找中介,不用直接和房东打交道,看房、签合同、交房租都通过中介处理;网购时,快递小哥帮你接收包裹,还会提醒你检查是否破损。这些"中间人"的存在,让原本直接的交互多了一层可控的环节——他们能帮你过滤信息、做些额外处理,甚至阻止不合理的操作。

在 JavaScript 中,也有这样的"中间人",它就是 ES6 引入的 Proxy。今天我们就从生活场景出发,一步步搞懂 Proxy 到底能做什么,以及怎么用。

什么是 Proxy?先看一个租房的例子

假设你有一个简单的对象,代表一套待租的房子:

const house = {
  price: 3000, // 月租
  area: 60, // 面积
  isRented: false // 是否已出租
};

如果直接操作这个对象,比如修改价格、标记出租,都是"直接和房东打交道":

house.price = 3500; // 直接改价格
house.isRented = true; // 直接标记出租

但如果有个"中介"(Proxy)介入呢?这个中介可以在你改价格时检查是否合理,在标记出租时记录时间,甚至阻止你做一些离谱的操作(比如把价格改成负数)。

这就是 Proxy 的核心作用:给目标对象找一个"中间人",所有对目标对象的操作,都会先经过这个中间人,我们可以在中间人中定义规则,决定怎么处理这些操作

怎么创建这个"中间人"?

创建 Proxy 很简单,只需要两个东西:

  1. 目标对象:你要代理的对象(比如上面的 house);
  2. 处理程序(handler):定义"中间人"的行为规则,里面有各种"陷阱"(trap),每个陷阱对应一种操作(比如读取属性、修改属性)。

基本语法长这样:

const proxy = new Proxy(目标对象, {
  // 处理程序:里面是各种陷阱
  get(目标对象, 属性名) {
    // 当读取属性时触发
  },
  set(目标对象, 属性名, 新值) {
    // 当修改属性时触发
  }
  // 还有其他陷阱...
});

接下来,我们通过具体场景,看看这些"陷阱"怎么用。

场景1:给属性读取加个"提示音"

假设我们想知道什么时候有人查看了房子的价格或面积,就像中介会记录"谁在什么时候问了房价"。

get 陷阱就能实现,它会在读取对象属性时触发:

const house = { price: 3000, area: 60, isRented: false };

// 创建代理(中间人)
const houseProxy = new Proxy(house, {
  // 读取属性时触发的陷阱
  get(target, prop) {
    // target 是原对象(house),prop 是要读取的属性名
    console.log(`有人查看了 ${prop},当前值:${target[prop]}`);
    // 必须返回属性值,否则读取不到
    return target[prop];
  }
});

// 试试读取属性
console.log(houseProxy.price); 
// 输出:有人查看了 price,当前值:3000 → 3000

console.log(houseProxy.area); 
// 输出:有人查看了 area,当前值:60 → 60

运行这段代码,每次读取 houseProxy 的属性,都会先打印一条日志,再返回实际值。这就是"中间人"在工作:它先记录操作,再把结果返回给你。

场景2:给属性修改加个"守门人"

租房时,房东可能会说"价格不能低于2000,也不能高于5000"。这时候中介(Proxy)就可以在你改价格时检查是否符合规则,不符合就拒绝。

set 陷阱可以实现,它会在修改对象属性时触发:

const house = { price: 3000, area: 60, isRented: false };

const houseProxy = new Proxy(house, {
  get(target, prop) {
    return target[prop];
  },
  // 修改属性时触发的陷阱
  set(target, prop, value) {
    // target:原对象;prop:属性名;value:新值
    if (prop === 'price') {
      if (value < 2000 || value > 5000) {
        console.log(`价格${value}不合理,必须在2000-5000之间`);
        return false; // 拒绝修改
      }
      // 合理就更新
      target[prop] = value;
      console.log(`价格已更新为${value}`);
      return true; // 表示修改成功
    }
    // 其他属性直接更新(比如isRented)
    target[prop] = value;
    return true;
  }
});

// 试试修改价格
houseProxy.price = 1500; 
// 输出:价格1500不合理,必须在2000-5000之间

houseProxy.price = 4000; 
// 输出:价格已更新为4000
console.log(houseProxy.price); // 输出:4000

// 修改isRented(不受限制)
houseProxy.isRented = true;
console.log(houseProxy.isRented); // 输出:true

这里的 set 陷阱就像一个守门人:检查修改是否符合规则,符合就放行,不符合就拦下。注意 set 陷阱必须返回布尔值(true 表示成功,false 表示失败)。

场景3:保护"核心信息"不被删除

有些对象的属性很重要,比如房子的面积(面积是固定的,不能被删除)。这时候可以用 deleteProperty 陷阱,阻止删除核心属性:

const house = { price: 3000, area: 60, isRented: false };

const houseProxy = new Proxy(house, {
  // 删除属性时触发的陷阱
  deleteProperty(target, prop) {
    if (prop === 'area') {
      console.log(`面积${prop}不能删除!`);
      return false; // 拒绝删除
    }
    // 其他属性可以删除
    delete target[prop];
    console.log(`已删除${prop}`);
    return true;
  }
});

// 试试删除area
delete houseProxy.area; 
// 输出:面积area不能删除!
console.log(houseProxy.area); // 输出:60(还在)

// 试试删除price
delete houseProxy.price; 
// 输出:已删除price
console.log(houseProxy.price); // 输出:undefined(已删除)

通过 deleteProperty 陷阱,我们成功保护了核心属性不被误删。

场景4:让对象"聪明"一点,支持"不存在的属性"

有时候,我们希望对象能处理一些"不存在的属性"。比如问房子"总价"(虽然原对象没有这个属性,但可以通过 price * 12 计算)。

get 陷阱可以实现这种"动态属性":

const house = { price: 3000, area: 60 };

const houseProxy = new Proxy(house, {
  get(target, prop) {
    // 如果是"totalPrice",返回年租金
    if (prop === 'totalPrice') {
      return target.price * 12;
    }
    // 如果是"pricePerArea",返回每平米价格
    if (prop === 'pricePerArea') {
      return (target.price / target.area).toFixed(2);
    }
    // 其他属性返回原值(如果不存在,返回undefined)
    return target[prop];
  }
});

console.log(houseProxy.totalPrice); // 输出:36000(3000*12)
console.log(houseProxy.pricePerArea); // 输出:50.00(3000/60)

这里的 Proxy 就像一个"智能中介":即使原对象没有某个属性,它也能根据已有信息动态计算并返回结果。

Proxy 能做的还有更多

除了上面的 getsetdeleteProperty,Proxy 还有很多陷阱,对应不同的操作:

  • has:检查对象是否有某个属性时触发(比如 'price' in houseProxy);
  • apply:当代理的是函数时,调用函数会触发;
  • construct:当代理的是构造函数时,用 new 创建实例会触发。

这些陷阱的用法和前面的例子类似,核心都是"在操作发生时插入自定义逻辑"。

为什么需要 Proxy?

简单说,Proxy 让我们能更灵活地控制对象的行为。比如:

  • 数据验证(像场景2中检查价格范围);
  • 日志记录(像场景1中跟踪属性读取);
  • 保护敏感属性(像场景3中阻止删除核心信息);
  • 动态计算属性(像场景4中生成总价)。

在框架中(比如 Vue 3),Proxy 被用来实现数据响应式——当数据变化时,自动更新页面,背后就是用 set 陷阱监控了数据的修改。

Proxy在Vue3中的应用核心逻辑拆解

Vue 3 的响应式本质是:

  1. 用 Proxy 包装数据(劫持 get/set 操作);
  2. 当读取数据时(get),记录“谁在用这个数据”(收集依赖);
  3. 当修改数据时(set),通知“所有用了这个数据的地方”重新执行(触发更新)。

简化版响应式实现(可直接运行)

// 1. 存储依赖的容器:记录“哪个对象的哪个属性,被哪些函数依赖”
// 结构:WeakMap<目标对象, Map<属性名, Set<依赖函数>>>
const targetMap = new WeakMap();

// 2. 收集依赖:当读取属性时,记录“当前正在执行的函数”为依赖
let activeEffect = null; // 临时存储“当前需要被收集的函数”
function track(target, prop) {
  if (!activeEffect) return; // 没有需要收集的函数,直接返回

  // 为目标对象创建依赖映射(如果没有)
  let propMap = targetMap.get(target);
  if (!propMap) {
    propMap = new Map();
    targetMap.set(target, propMap);
  }

  // 为属性创建依赖集合(如果没有)
  let effects = propMap.get(prop);
  if (!effects) {
    effects = new Set();
    propMap.set(prop, effects);
  }

  // 将当前函数加入依赖集合
  effects.add(activeEffect);
}

// 3. 触发更新:当修改属性时,执行所有依赖该属性的函数
function trigger(target, prop) {
  const propMap = targetMap.get(target);
  if (!propMap) return; // 没有依赖,直接返回

  const effects = propMap.get(prop);
  if (effects) {
    // 执行所有依赖函数
    effects.forEach(effect => effect());
  }
}

// 4. 创建响应式对象:用 Proxy 包装目标对象,劫持 get/set
function reactive(target) {
  return new Proxy(target, {
    get(target, prop) {
      // 读取属性时,收集依赖
      track(target, prop);
      return target[prop]; // 返回属性值
    },
    set(target, prop, value) {
      if (target[prop] === value) return; // 值没变,不触发更新
      target[prop] = value; // 更新属性值
      // 修改属性时,触发更新
      trigger(target, prop);
      return true;
    }
  });
}

// 5. 注册副作用函数:需要“响应式执行”的函数(比如更新页面的逻辑)
function watchEffect(effect) {
  activeEffect = effect; // 标记当前函数为“需要被收集的依赖”
  effect(); // 执行一次函数(触发 get 操作,从而收集依赖)
  activeEffect = null; // 重置,避免误收集
}

测试:数据变了,页面自动更新

我们用上面的工具模拟“数据更新后自动刷新页面”的场景:

// 原始数据
const data = {
  name: "Vue",
  version: "3.x",
  count: 0
};

// 创建响应式数据(用 Proxy 包装)
const reactiveData = reactive(data);

// 注册“更新页面”的副作用函数(模拟页面渲染)
watchEffect(() => {
  console.log(`页面更新:name=${reactiveData.name}, version=${reactiveData.version}, count=${reactiveData.count}`);
});
// 首次执行会输出:页面更新:name=Vue, version=3.x, count=0


// 测试修改数据,观察是否自动更新
reactiveData.count++; 
// 输出:页面更新:name=Vue, version=3.x, count=1

reactiveData.name = "Vue 3"; 
// 输出:页面更新:name=Vue 3, version=3.x, count=1

reactiveData.version = "3.4"; 
// 输出:页面更新:name=Vue 3, version=3.4, count=1

核心逻辑说明

  1. reactive 函数:用 Proxy 包装数据,每次读取属性(get)时调用 track 收集依赖,每次修改属性(set)时调用 trigger 触发更新。
  2. watchEffect 函数:注册一个“副作用函数”(比如页面渲染逻辑),并主动执行一次。执行时会读取响应式数据,触发 Proxy 的 get 陷阱,从而通过 track 记录“这个函数依赖了哪些属性”。
  3. track 和 triggertrack 负责把“当前函数”和“它用到的属性”关联起来;trigger 负责在属性变化时,找到所有关联的函数并重新执行。

和Vue 3 源码的关系

上面是极简版实现,Vue 3 源码会更复杂(比如处理数组、嵌套对象、循环依赖、懒加载等),但核心逻辑完全一致:

  • 用 Proxy 劫持数据操作;
  • 在 get 时收集“谁依赖了这个数据”;
  • 在 set 时通知“依赖者”重新执行。

这就是为什么修改 Vue 3 的响应式数据时,页面会自动更新——背后正是 Proxy 在“暗中监控”并触发更新逻辑。

最后:动手试试

把上面的代码复制到浏览器控制台(F12 打开),或者 Node.js 环境中运行,感受一下 Proxy 这个"中间人"的工作方式。试着改改陷阱里的逻辑,比如给 set 陷阱加一个"isRented 为 true 后不能再改价格"的规则,看看能不能实现。

Proxy 不难,记住它就是一个"可控的中间人"——所有操作先过它的手,我们可以在中间加任何想加的逻辑。理解了这一点,你就已经掌握了它的核心。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐