在这里插入图片描述

前端进阶必学:用 Proxy 对象轻松拦截与控制 JavaScript 对象行为

前端进阶必学:用 Proxy 对象轻松拦截与控制 JavaScript 对象行为

引言:Vue 3 的“黑魔法”源头,其实你也可以拥有

“响应式”这个词,在前端圈已经热到发烫。
Vue 3 一发布,大家最津津乐道的就是“用 Proxy 重写的响应式系统,比 Vue 2 快、准、狠”。
很多人第一次听到 Proxy 时,以为又是“面试造火箭”系列,结果看完文档才发现——这玩意儿不仅能造火箭,还能顺手把自家后院的小菜地也自动化了:校验、缓存、日志、mock、DSL……几乎无所不能。

今天这篇文章,咱们就把 Proxy 从“高冷黑科技”拉到“工地现场”,用一堆能直接复制粘贴的代码,把它玩成一把瑞士军刀。
读完你不仅能徒手撸一个迷你 Vue 3,还能顺手给祖传代码加上“防呆提示”,甚至把 localStorage 玩出花来。
准备好咖啡,咱们开始“偷窥”对象的一生。


Proxy 是什么?能吃吗?——中间人、偷窥狂、还是贴心小棉袄?

一句话版本:
Proxy 就是对象的“中间人”,任何对目标对象的“小动作”都必须先经过它。
想读属性?先问它。想改属性?先报审。想删属性?先签字。
它心情好,就放行;心情不好,就给你抛错误、改数据、打日志,甚至直接“狸猫换太子”。

技术定义(怕面试问到,还是背一下):

new Proxy(target, handler) 会生成一个“代理”对象,对代理对象的所有操作都会被 handler 里的 trap(陷阱)拦截。

能吃吗?
能,而且越嚼越香。下面先啃一口最基础的语法,顺便把“陷阱”都踩一遍。


基本语法与核心陷阱:先搭个“偷窥”脚手架

// 目标对象:一个朴实无华的打工人
const user = { name: '小明', age: 18, salary: 5000 };

// 处理器:偷窥、篡改、放行,一条龙服务
const handler = {
  // 读属性时触发
  get(target, key, receiver) {
    console.log(`[GET] 有人偷偷读取了 ${key}`);
    return target[key];
  },

  // 写属性时触发
  set(target, key, value, receiver) {
    console.log(`[SET] 有人想把 ${key} 改成 ${value}`);
    target[key] = value;
    return true; // 返回 true 表示写入成功
  },

  // in 操作符触发
  has(target, key) {
    console.log(`[HAS] 有人在打听 ${key} 是否存在`);
    return key in target;
  },

  // delete 操作符触发
  deleteProperty(target, key) {
    console.log(`[DELETE] 有人想干掉 ${key}`);
    delete target[key];
    return true;
  }
};

// 生成代理对象
const proxyUser = new Proxy(user, handler);

// 试运行
proxyUser.name;          // [GET] 有人偷偷读取了 name
proxyUser.age = 19;      // [SET] 有人想把 age 改成 19
'age' in proxyUser;      // [HAS] 有人在打听 age 是否存在
delete proxyUser.salary; // [DELETE] 有人想干掉 salary

上面这段代码,就是 Proxy 的“Hello World”。
注意三个小细节,稍不留神就翻车:

  1. set 必须返回 true,否则严格模式下会抛 TypeError
  2. deleteProperty 也必须返回 true,否则同样报错。
  3. receiver 就是代理对象本身,别跟 target 搞混;在链式代理或继承场景里,它会成为 this 的指向依据。

常见 trap 深度剖析:别只会在 get/set 里打转

官方一共 13 个 trap,名字都起得很“中二”,但个个身怀绝技。
下面挑几个“面试常问、实战常用”的,配上“人话”解释和“能跑”的代码。

1. apply —— 函数调用的“拦路虎”

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

const proxySum = new Proxy(sum, {
  // 函数被调用时触发
  apply(target, thisArg, args) {
    console.log(`[APPLY] 有人求和:${args}`);
    // 顺便做个类型检查,防止有人传字符串
    if (args.some(v => typeof v !== 'number')) {
      throw new TypeError('求和函数只接受数字');
    }
    return Reflect.apply(target, thisArg, args);
  }
});

proxySum(3, 4);   // [APPLY] 有人求和:3,4  → 7
proxySum(3, '4'); // 抛错

2. construct —— new 操作的“门禁”

class Person {
  constructor(name) {
    this.name = name;
  }
}

const ProxyPerson = new Proxy(Person, {
  construct(target, args) {
    console.log(`[CONSTRUCT] 有人 new 了一个 ${target.name}`);
    // 可以在这里做“单例”或“对象池”
    return new target(...args);
  }
});

const p = new ProxyPerson('小红'); // [CONSTRUCT] 有人 new 了一个 Person

3. ownKeys —— 决定遍历出来哪些属性

const secrets = { public: 'hello', _password: '123456' };

const safe = new Proxy(secrets, {
  ownKeys(target) {
    // 把下划线开头的“隐藏”掉
    return Object.keys(target).filter(k => !k.startsWith('_'));
  }
});

console.log(Object.keys(safe)); // ["public"]

4. defineProperty —— 拦截 Object.defineProperty

const obj = {};
const readonly = new Proxy(obj, {
  defineProperty(target, key, descriptor) {
    if (descriptor.writable) {
      throw new Error('该对象禁止添加可写属性');
    }
    return Object.defineProperty(target, key, descriptor);
  }
});

Object.defineProperty(readonly, 'a', { value: 1, writable: false }); // OK
Object.defineProperty(readonly, 'b', { value: 2, writable: true });  // Error

优势在哪里?——为什么 Vue 3 抛弃 definedProperty 奔向 Proxy

先放一张“吐槽大会”对比表:

能力 Object.defineProperty Proxy
拦截新增/删除属性 ❌ 需要额外 s e t / set/ set/delete ✅ 直接监听
拦截数组索引/length ❌ 需要重写数组方法 ✅ 直接监听
拦截 in、delete、遍历 ❌ 不支持 ✅ 全部支持
性能(巨量读取) 需要递归劫持 惰性拦截,首次快
代码体积 需要大量辅助函数 官方内置,无额外代码

一句话总结:
defineProperty 是“装修老房子”,砸墙补洞;
Proxy 是“直接盖新楼”,想怎么改就怎么改,还不用动承重墙。


局限和坑点:别一激动就把老项目全重构了

  1. 兼容性
    IE 全系不支持,Edge 12+ 才起步。
    无法被 polyfill:Proxy 的陷阱在语言层面,再高阶的 Babel 也救不了。
    解法:

    • 业务面向 C 端且需要兼容?乖乖用回 defineProperty 或框架封装。
    • 内部后台、Electron、小程序?放心飞。
  2. 性能误区
    单次 get 会比原生慢 3~5 倍,但在“巨量读取”场景里,由于 Proxy 惰性代理,整体性能反而优于一次性递归劫持。
    结论:

    • 对性能极度敏感(比如游戏循环)慎用;
    • 普通业务代码,差距可忽略。
  3. this 指向
    代理对象与目标对象的 this 并不总是保持一致。
    典型翻车:

    const target = {
      foo() { return this.bar; },
      bar: 1
    };
    const proxy = new Proxy(target, {});
    proxy.foo(); // 1,没问题
    // 但如果 foo 被“提取”出来
    const { foo } = proxy;
    foo(); // undefined,因为 this 丢失
    

    解决:用 Reflect.get/Reflect.setreceiver 传进去,或在 handler 里手动绑定。


实战一:带验证规则的对象——让错误在萌芽期就被拍死

// 规则描述:每个 key 对应一个校验函数
const rules = {
  email: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || '邮箱格式非法',
  age: v => (v >= 18 && v <= 60) || '年龄必须在 18-60 之间'
};

// 工厂函数:根据规则生成代理
function createValidator(target, rules) {
  return new Proxy(target, {
    set(target, key, value) {
      const rule = rules[key];
      if (rule) {
        const result = rule(value);
        if (result !== true) {
          throw new TypeError(`属性 ${key} 校验失败:${result}`);
        }
      }
      target[key] = value;
      return true;
    }
  });
}

// 使用
const user = createValidator({}, rules);
user.email = 'foo@bar.com'; // OK
user.age = 17;              // 抛错:年龄必须在 18-60 之间

实战二:迷你响应式系统——30 行代码体验 Vue 3 核心

// 依赖收集桶
const targetMap = new WeakMap();

let activeEffect = null;
function effect(fn) {
  activeEffect = fn;
  fn();          // 立即执行一次,触发 get
  activeEffect = null;
}

// 响应式工厂
function reactive(target) {
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 1. 收集依赖
      if (activeEffect) {
        let depsMap = targetMap.get(target);
        if (!depsMap) targetMap.set(target, depsMap = new Map());
        let deps = depsMap.get(key);
        if (!deps) depsMap.set(key, deps = new Set());
        deps.add(activeEffect);
      }
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        // 2. 触发更新
        const depsMap = targetMap.get(target);
        if (depsMap) {
          const deps = depsMap.get(key);
          deps && deps.forEach(fn => fn());
        }
      }
      return result;
    }
  });
  return proxy;
}

// 测试
const state = reactive({ count: 0 });
effect(() => {
  console.log('count 现在是:', state.count);
});
state.count++; // 自动打印:count 现在是:1

就这么点代码,实现了“数据变 → 视图变”的核心链路。
Vue 3 在在此基础上加了调度系统、批量更新、深度嵌套,但本质相同。
面试时把这段手写出去,面试官多半会“嗯”一声,然后给你加 20 分。


实战三:拦截未定义属性,自动提示拼写错误

const dictionary = ['name', 'age', 'salary'];

const smartObj = new Proxy({}, {
  get(target, key) {
    if (key in target) return target[key];
    // 模糊匹配,提示最接近的单词
    const suggestion = dictionary.find(w => w.startsWith(key.slice(0, 2)));
    if (suggestion) {
      throw new ReferenceError(`属性 ${key} 未定义,你是不是想写 ${suggestion}`);
    }
    throw new ReferenceError(`属性 ${key} 未定义`);
  }
});

smartObj.nam; // 抛错:属性 nam 未定义,你是不是想写 name?

实战四:懒加载配置对象——昂贵资源按需初始化

function createLazyLoader(loader) {
  let cache;
  let loaded = false;
  return new Proxy({}, {
    get(target, key) {
      if (!loaded) {
        console.log('[Lazy] 第一次访问,开始加载...');
        cache = loader();
        loaded = true;
      }
      return cache[key];
    }
  });
}

// 假设加载配置需要 1s
const config = createLazyLoader(() => {
  // 模拟昂贵操作
  const res = JSON.parse(require('fs').readFileSync('./heavy-config.json'));
  return res;
});

// 只有第一次访问才会真正读文件
console.log(config.apiUrl);

调试与排错:当代理对象在控制台里“隐身”

  1. console.log 打印出来像“空对象”?
    因为控制台展开的是 target 的原始快照,而 Proxy 不会自动列出属性。
    解决:

    console.log([...Object.keys(proxy)]);
    // 或者
    console.log(JSON.stringify(proxy));
    
  2. 不知道哪个 trap 被触发?
    给 handler 包一层“日志装饰器”:

    function trace(handler) {
      return new Proxy(handler, {
        get(target, trap) {
          return function(...args) {
            console.log(`[TRACE] 触发 trap:${trap}`, args);
            return target[trap](...args);
          };
        }
      });
    }
    const proxy = new Proxy(target, trace(handler));
    
  3. DevTools 的 “Break on property access” 对 Proxy 无效?
    因为断点打在 target 上,而代码操作的是代理。
    解决:在 handler 里手动 debugger;,或者把代理对象挂到全局 window.__p = proxy,然后在控制台里访问 __p.xxx,断点即可生效。


提升效率的小技巧:让 Proxy 成为你开发时的“隐形助手”

1. 高性能缓存代理(Memoize)

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

const fib = memoize(n => (n <= 1 ? n : fib(n - 1) + fib(n - 2)));
console.log(fib(40)); // 第一次算,第二次瞬间返回

2. 结合 Reflect,让代码更“合法”

// 不推荐:直接 target[key] = value
// 推荐:Reflect.set(target, key, value, receiver)
// 原因:Reflect 会正确触发 setter、代理链,并返回 boolean,减少暗坑。

3. 测试时 mock 模块,不动源码

// 假设原模块
// math.js
export const add = (a, b) => a + b;

// 测试文件
import * as math from './math';
const mockMath = new Proxy(math, {
  get(target, key) {
    if (key === 'add') return () => 42; // 强制返回 42
    return target[key];
  }
});
// 把 mockMath 注入被测模块即可,无需改源码

4. 拦截 localStorage,统一序列化 + 过期逻辑

const storage = new Proxy(localStorage, {
  get(target, key) {
    try {
      const raw = target[key];
      if (!raw) return null;
      const { value, expire } = JSON.parse(raw);
      if (expire && Date.now() > expire) {
        delete target[key];
        return null;
      }
      return value;
    } catch {
      return target[key];
    }
  },
  set(target, key, value) {
    const payload = { value, expire: Date.now() + 86400000 }; // 一天过期
    target[key] = JSON.stringify(payload);
    return true;
  }
});

storage.token = 'abc123'; // 自动带过期时间

彩蛋:花式玩法,打开元编程的大门

1. 链式调用 DSL——写 SQL 像写诗

const sql = new Proxy({}, {
  get(target, key) {
    return function(...args) {
      console.log(`[SQL] ${key.toUpperCase()} ${args.join(' ')}`);
      return sql; // 继续链式
    };
  }
});

sql.select('*').from('users').where('id = 1');
// 打印:
// [SQL] SELECT *
// [SQL] FROM users
// [SQL] WHERE id = 1

2. 动态 API 客户端——后端加字段,前端零改动

function createClient(baseURL) {
  return new Proxy({}, {
    get(target, key) {
      return function(data) {
        return fetch(`${baseURL}/${key}`, {
          method: 'POST',
          body: JSON.stringify(data)
        }).then(res => res.json());
      };
    }
  });
}

const api = createClient('/api');
api.login({ user: 'foo', pass: 'bar' }).then(console.log);
// 后端新增 /api/logout,前端无需改代码,直接 api.logout() 即可

3. 简易 AOP——给任何函数前后加“外挂”

function withLog(fn) {
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      console.log(`[Before] 参数:${args}`);
      const result = Reflect.apply(target, thisArg, args);
      console.log(`[After] 结果:${result}`);
      return result;
    }
  });
}

const add = withLog((a, b) => a + b);
add(3, 4);
// [Before] 参数:3,4
// [After] 结果:7

收个尾:Proxy 不是银弹,但它是万能瑞士军刀

写到最后,你会发现 Proxy 最大的魅力不是“性能”,也不是“酷炫”,而是让代码保持干净的同时,拥有无限可能
它像一道隐形门,把“业务逻辑”与“横切关注点”彻底隔开:

  • 校验、缓存、日志、mock、权限、过期、懒加载……
  • 全部以“非侵入”的方式插拔,想加就加,想拔就拔,核心业务代码稳如老狗。

所以,下次遇到“又要改老代码,又怕牵一发动全身”的窘境时,不妨掏出 Proxy,悄悄在暗处装上一道拦截网。
你会发现,原来“优雅”与“务实”并不冲突——它们只差一个 new Proxy()

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁) 学习路线(点击解锁) 知识定位
《微信小程序相关博客》 持续更新中~ 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》 持续更新中~ AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》 Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》 持续更新中~ 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》 持续更新中~ Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》 持续更新中~ SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》 持续更新中~ 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》 持续更新中~ 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》 持续更新中~ 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》 持续更新中~ 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》 持续更新中~ 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

在这里插入图片描述

Logo

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

更多推荐