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

前端进阶必学:用 Proxy 对象轻松拦截与控制 JavaScript 对象行为
- 前端进阶必学:用 Proxy 对象轻松拦截与控制 JavaScript 对象行为
-
- 引言:Vue 3 的“黑魔法”源头,其实你也可以拥有
- Proxy 是什么?能吃吗?——中间人、偷窥狂、还是贴心小棉袄?
- 基本语法与核心陷阱:先搭个“偷窥”脚手架
- 常见 trap 深度剖析:别只会在 get/set 里打转
- 优势在哪里?——为什么 Vue 3 抛弃 definedProperty 奔向 Proxy
- 局限和坑点:别一激动就把老项目全重构了
- 实战一:带验证规则的对象——让错误在萌芽期就被拍死
- 实战二:迷你响应式系统——30 行代码体验 Vue 3 核心
- 实战三:拦截未定义属性,自动提示拼写错误
- 实战四:懒加载配置对象——昂贵资源按需初始化
- 调试与排错:当代理对象在控制台里“隐身”
- 提升效率的小技巧:让 Proxy 成为你开发时的“隐形助手”
- 彩蛋:花式玩法,打开元编程的大门
- 收个尾:Proxy 不是银弹,但它是万能瑞士军刀
前端进阶必学:用 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”。
注意三个小细节,稍不留神就翻车:
set必须返回true,否则严格模式下会抛TypeError。deleteProperty也必须返回true,否则同样报错。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 是“直接盖新楼”,想怎么改就怎么改,还不用动承重墙。
局限和坑点:别一激动就把老项目全重构了
-
兼容性
IE 全系不支持,Edge 12+ 才起步。
无法被 polyfill:Proxy 的陷阱在语言层面,再高阶的 Babel 也救不了。
解法:- 业务面向 C 端且需要兼容?乖乖用回
defineProperty或框架封装。 - 内部后台、Electron、小程序?放心飞。
- 业务面向 C 端且需要兼容?乖乖用回
-
性能误区
单次get会比原生慢 3~5 倍,但在“巨量读取”场景里,由于 Proxy 惰性代理,整体性能反而优于一次性递归劫持。
结论:- 对性能极度敏感(比如游戏循环)慎用;
- 普通业务代码,差距可忽略。
-
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.set把receiver传进去,或在 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);
调试与排错:当代理对象在控制台里“隐身”
-
console.log 打印出来像“空对象”?
因为控制台展开的是target的原始快照,而 Proxy 不会自动列出属性。
解决:console.log([...Object.keys(proxy)]); // 或者 console.log(JSON.stringify(proxy)); -
不知道哪个 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)); -
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等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

更多推荐


所有评论(0)