的,大纲收到了!这大纲写得挺实在的,“唠唠“、“坑过的惨痛经历“、“老铁“这些词儿一看就不是AI
写到这儿,估计你也看累了。说实话,Proxy这东西,刚学的时候觉得"哇好牛逼",用的时候觉得"卧槽怎么又出bug了",用熟了之后觉得"真香,离不开了"。技术这东西就是这样,没有银弹。Proxy很强,但不是万能的。简单场景别硬上,复杂场景别害怕。多写代码多踩坑,踩多了你就成大神了。你看我写了这么多代码示例,你不跑一遍,根本不知道自己哪里没理解。最好是看完一个例子,自己不看代码重新写一遍,写不出来再回
的,大纲收到了!这大纲写得挺实在的,"唠唠"、"坑过的惨痛经历"、"老铁"这些词儿一看就不是AI
好的,大纲收到了!这大纲写得挺实在的,“唠唠”、“坑过的惨痛经历”、“老铁"这些词儿一看就不是AI写的。我来给你整一篇像微信群里语音转文字那种风格的,可能会碎碎念、可能会突然插一句"卧槽这个坑我踩过”,但绝对真实。文章会很长,代码会很多,咱们开始吧!
前端进阶必看:3天吃透ES6 Proxy对象(实战案例+避坑指南)
开篇先唠唠这玩意儿到底是个啥
说实话,我第一次听说Proxy的时候,脑子里第一反应是"这啥啊?代理?VPN那种代理吗?"后来一看文档,哦,原来是ES6搞出来的一个新玩具,专门用来拦截对象操作的。当时我就想说,JavaScript你咋越来越像Java了,整这么多花里胡哨的。
但真香定律可能会迟到,但永远不会缺席。用了Proxy之后,我只想说:Object.defineProperty是个啥?真不熟。
那些年我们被Object.defineProperty坑过的惨痛经历
还记得Vue2的响应式原理吗?当时面试官问你"Vue是怎么实现数据绑定的",你得背那一套:遍历对象所有属性,用Object.defineProperty给每个属性加getter和setter。听起来挺美好的是吧?实际操作起来,那叫一个酸爽。
首先,Object.defineProperty只能拦截已经存在的属性。你后面动态给对象加个新属性?不好意思,监听不到。这就是为什么Vue2里你要用Vue.set或者this.$set,不然视图就是不更新。我当时就纳闷了,我明明给data里的对象加了新属性,控制台打印也有了,页面上就是没反应,排查了半天才发现是这破玩意儿的问题。
其次,数组的监听更是一言难尽。Object.defineProperty不能监听数组索引的变化,也不能监听数组长度的变化。Vue2为了解决这个问题,只能重写数组的那七个方法:push、pop、shift、unshift、splice、sort、reverse。你说这代码写得憋屈不憋屈?我改个arr[0] = 1,监听不到;我改个arr.length = 0,也监听不到。非得让我用arr.splice(0, 1, 1)这种反人类的写法。
还有性能问题。对象层级一深,递归遍历所有属性去定义getter/setter,那性能损耗肉眼可见。对象里有1000个属性?等着卡顿吧。
最离谱的是,Object.defineProperty不能监听对象本身的变化。你直接给整个对象重新赋值?监听个寂寞。
Proxy到底能帮前端老铁解决哪些痛点
ES6看不下去了,说你们别折腾了,我给你们整点新活。于是Proxy横空出世。
Proxy能干啥?简单说,它就是个中间商。你想操作对象?行,先过我这一关。读取属性、设置属性、删除属性、遍历属性、函数调用、new操作符,统统能拦截。而且Proxy是对整个对象的代理,不是针对单个属性。你动态加属性?拦得住。你改数组索引?拦得住。你删属性?也拦得住。
最爽的是,Proxy返回的是一个新对象,原对象还在那儿,你想咋玩咋玩。不像Object.defineProperty,直接污染原对象,改得面目全非。
看完这篇你能获得什么技能包
兄弟,看完这篇你能干啥?首先,Vue3的响应式原理你能看懂了,甚至自己能写一个简易版。其次,你能用Proxy做很多以前想都不敢想的事情:自动化的日志埋点、接口请求的loading状态管理、权限校验、数据格式转换,甚至能自己实现一个简单的双向绑定框架。
最重要的是,面试时候你能跟面试官吹了:"Proxy?那玩意儿我熟啊,我自己用Proxy写过响应式系统。"面试官一听,眼睛都亮了。
Proxy到底是啥来头
用大白话解释Proxy就是个中间商赚差价
咱们不搞那些官方术语,就用大白话说。Proxy,翻译过来是"代理"。在现实生活中,代理是啥?比如说你想买双限量版的AJ,官网抢不到,你找个黄牛(代理),黄牛帮你去排队、去抢购,最后把鞋给你。在这个过程中,黄牛可以加价(拦截操作并修改),也可以直接说"这鞋没了"(拦截并阻止),甚至可以给你塞双假鞋(返回假数据)。
Proxy在JavaScript里就是这个角色。你本来直接操作对象,现在你先操作Proxy,Proxy再决定要不要把这个操作转给真正的对象,以及怎么转。
target、handler、trap这三个核心概念掰开揉碎讲
Proxy的语法其实很简单:
const proxy = new Proxy(target, handler);
就两个参数。第一个target,就是你要代理的那个"原对象",也就是上面例子里的"限量版AJ"。第二个handler,是一个对象,里面定义了各种拦截操作,也就是"黄牛的行为准则"。
handler里面可以定义很多方法,这些方法叫做trap(陷阱)。为啥叫陷阱?因为对象操作一踩进来,就被你抓住了。常见的trap有:
- get:拦截读取属性操作
- set:拦截设置属性操作
- has:拦截in操作符
- deleteProperty:拦截delete操作
- ownKeys:拦截Object.keys()、for…in等遍历操作
- apply:拦截函数调用
- construct:拦截new操作
咱们一个个来看,先整点简单的代码热热身:
// 原对象,也就是那个"限量版AJ"
const target = {
name: 'Air Jordan 1',
price: 1499
};
// handler,定义黄牛的行为
const handler = {
// get陷阱:有人来看鞋的时候
get(target, prop) {
console.log(`有人正在查看属性:${prop}`);
// 返回原对象的属性值
return target[prop];
},
// set陷阱:有人要改价格的时候
set(target, prop, value) {
console.log(`有人正在设置属性:${prop} = ${value}`);
// 把新值设置到原对象上
target[prop] = value;
return true; // 严格模式下必须返回true表示设置成功
}
};
// 创建代理
const proxy = new Proxy(target, handler);
// 试试效果
console.log(proxy.name); // 会打印"有人正在查看属性:name",然后输出"Air Jordan 1"
proxy.price = 1999; // 会打印"有人正在设置属性:price = 1999"
看到没?就这么简单。你在操作proxy的时候,实际上触发了handler里的trap,trap里面你可以干任何事情:记录日志、修改返回值、阻止操作,甚至返回完全不相干的数据。
Proxy和Reflect这对CP的相爱相杀关系
说到Proxy,就不得不提Reflect。这俩是ES6一起出来的,天生一对。Reflect是啥?它是一个内置对象,提供了一堆操作对象的方法,而且这些方法的名字和Proxy的trap一一对应。
比如Proxy有get陷阱,Reflect就有Reflect.get方法;Proxy有set陷阱,Reflect就有Reflect.set方法。
那为啥要有Reflect?直接用target[prop]不香吗?香是香,但Reflect有几个好处:
第一,Reflect的方法返回值更合理。比如Reflect.set返回一个布尔值,表示是否设置成功;而target.prop = value这种写法,你根本不知道设置成功没。
第二,Reflect可以处理this指向问题,这个后面会讲到。
第三,也是最重要的,Reflect和Proxy配合起来用,代码更规范。看下面这个例子:
const handler = {
get(target, prop, receiver) {
// 老写法
// return target[prop];
// 新写法,更规范
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
// 老写法
// target[prop] = value;
// return true;
// 新写法
return Reflect.set(target, prop, value, receiver);
}
};
看到那个receiver参数没?这是Proxy的get和set陷阱的第三个参数,表示原始操作所在的对象。用Reflect的时候把这个receiver传进去,能保证this指向正确。这个很重要,后面讲坑的时候会详细说。
跟Vue2和Vue3的响应式原理扯上关系
前面吐槽了半天Object.defineProperty,现在来说说Vue3是怎么用Proxy的。
Vue3的响应式系统核心就是Proxy。它不再递归遍历对象的所有属性去定义getter/setter,而是直接代理整个对象。你动态加属性?Proxy拦得住。你改数组?Proxy也拦得住。你删属性?照样拦得住。
而且Vue3用了WeakMap来存储依赖关系,性能比Vue2的Dep类好很多。对象没了,依赖自动回收,不用担心内存泄漏。
咱们自己实现一个超简易版的Vue3响应式,你就明白Proxy有多香了:
// 存储依赖的桶
const bucket = new WeakMap();
// 当前正在执行的副作用函数
let activeEffect = null;
// 注册副作用函数的函数,相当于Vue的watchEffect
function effect(fn) {
activeEffect = fn;
fn(); // 立即执行一次,触发依赖收集
activeEffect = null;
}
// 响应式函数,相当于Vue3的reactive
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key);
// 用Reflect获取值,保证this指向正确
const result = Reflect.get(target, key, receiver);
// 如果值是对象,递归代理,实现深度响应式
if (result !== null && typeof result === 'object') {
return reactive(result);
}
return result;
},
set(target, key, newVal, receiver) {
// 先获取旧值,判断是否真的改变了
const oldVal = target[key];
// 设置新值
const result = Reflect.set(target, key, newVal, receiver);
// 只有值真的变了,才触发更新
if (oldVal !== newVal) {
trigger(target, key);
}
return result;
}
});
}
// 依赖收集
function track(target, key) {
if (!activeEffect) return; // 没有正在执行的副作用函数,直接返回
// 获取target对应的depsMap
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 获取key对应的依赖集合
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 把当前副作用函数加到依赖集合里
deps.add(activeEffect);
}
// 触发更新
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
// 执行所有依赖这个key的副作用函数
deps.forEach(fn => fn());
}
}
// 测试一下
const state = reactive({ count: 0 });
effect(() => {
console.log('count变了,现在是:', state.count);
});
// 输出:count变了,现在是: 0
state.count++;
// 输出:count变了,现在是: 1
state.count++;
// 输出:count变了,现在是: 2
看到没?就这么几十行代码,实现了一个基本的响应式系统。而且支持动态添加属性,支持嵌套对象,比Vue2的实现简洁多了。这就是Proxy的威力。
手把手教你玩转Proxy的18种拦截操作
Proxy一共支持13种拦截操作,但有些不常用,咱们挑重要的、常用的来讲,保证你学完就能上手用。
get拦截怎么搞,读取属性时偷偷加点料
get是最常用的陷阱了,拦截对象属性的读取操作。触发场景包括:点号访问(obj.prop)、方括号访问(obj[prop])、甚至解构赋值都会触发。
来看个实用的例子,实现一个默认值功能:
function withDefaults(target, defaults) {
return new Proxy(target, {
get(target, prop) {
// 如果对象本身有这个属性,返回对象本身的
if (prop in target) {
return target[prop];
}
// 否则返回默认值
return defaults[prop];
}
});
}
const user = withDefaults(
{ name: '张三' },
{ name: '匿名用户', age: 18, role: '普通用户' }
);
console.log(user.name); // 张三(对象本身有)
console.log(user.age); // 18(使用默认值)
console.log(user.role); // 普通用户(使用默认值)
再来个更骚的,实现属性名映射。有时候后端返回的数据是下划线命名(snake_case),前端想用驼峰命名(camelCase),以前你得手动转换,现在用Proxy自动搞定:
function toCamelCase(str) {
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
}
function createCamelProxy(target) {
return new Proxy(target, {
get(target, prop) {
// 把驼峰转回下划线,去原对象里找
const snakeProp = prop.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
if (snakeProp in target) {
return target[snakeProp];
}
// 如果找不到,试试直接用原属性名(可能本来就是下划线)
return target[prop];
}
});
}
const serverData = {
user_name: '李四',
user_age: 25,
is_vip: true
};
const user = createCamelProxy(serverData);
console.log(user.userName); // 李四
console.log(user.userAge); // 25
console.log(user.isVip); // true
看到没?后端给的是user_name,前端直接用user.userName,Proxy在中间自动帮你转换了。这代码写在数据层,业务代码里全是干净的驼峰命名,爽不爽?
get陷阱还有个高级用法,就是实现负索引数组。Python里可以用arr[-1]取最后一个元素,JavaScript不行,但有了Proxy,咱们自己实现:
function createNegativeArray(arr) {
return new Proxy(arr, {
get(target, prop) {
// 把属性名转成数字
const index = Number(prop);
// 如果是负数,从后面数
if (!isNaN(index) && index < 0) {
return target[target.length + index];
}
// 否则正常返回
return target[prop];
}
});
}
const arr = createNegativeArray([1, 2, 3, 4, 5]);
console.log(arr[0]); // 1
console.log(arr[-1]); // 5(最后一个)
console.log(arr[-2]); // 4(倒数第二个)
console.log(arr.at(-1)); // 其实ES2022已经支持at方法了,但这个例子展示了Proxy的灵活性
set拦截实战,赋值时候能玩出什么花样
set陷阱用来拦截属性赋值操作。注意,它拦截的是赋值操作,不是设置行为本身。如果你用Object.defineProperty定义了一个只读属性,那set陷阱根本触发不了,因为赋值操作被阻止了。
set陷阱必须返回一个布尔值,true表示设置成功,false表示设置失败。严格模式下,返回false会报错。
来看个数据校验的例子,这个在实际项目中超级实用:
function createValidator(target, validators) {
return new Proxy(target, {
set(target, prop, value) {
// 检查是否有对应的校验器
if (validators[prop]) {
const validator = validators[prop];
// 如果校验器是函数,直接调用
if (typeof validator === 'function') {
if (!validator(value)) {
console.error(`属性 ${prop} 的值 ${value} 校验失败`);
return false; // 阻止设置
}
}
// 如果校验器是正则表达式
else if (validator instanceof RegExp) {
if (!validator.test(value)) {
console.error(`属性 ${prop} 的值 ${value} 不符合正则 ${validator}`);
return false;
}
}
}
// 校验通过,设置值
target[prop] = value;
return true;
}
});
}
// 使用
const user = createValidator(
{ name: '', age: 0, email: '' },
{
name: val => val.length >= 2 && val.length <= 20, // 名字2-20个字符
age: val => val >= 0 && val <= 150, // 年龄0-150
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ // 邮箱正则
}
);
user.name = '张三'; // 成功
user.name = '张'; // 失败,太短了
user.age = 200; // 失败,太大了
user.email = 'invalid'; // 失败,不符合邮箱格式
user.email = 'test@example.com'; // 成功
再来个更实用的,实现私有属性。JavaScript的类里有私有属性(#prop),但对象字面量没有。用Proxy可以模拟:
function createPrivateProxy(target, privateKeys) {
const privateSet = new Set(privateKeys);
return new Proxy(target, {
get(target, prop) {
if (privateSet.has(prop)) {
throw new Error(`属性 ${prop} 是私有的,无法访问`);
}
return target[prop];
},
set(target, prop, value) {
if (privateSet.has(prop)) {
throw new Error(`属性 ${prop} 是私有的,无法修改`);
}
target[prop] = value;
return true;
},
ownKeys(target) {
// 遍历时过滤掉私有属性
return Object.keys(target).filter(key => !privateSet.has(key));
},
getOwnPropertyDescriptor(target, prop) {
if (privateSet.has(prop)) {
return undefined; // 私有属性没有描述符
}
return Object.getOwnPropertyDescriptor(target, prop);
}
});
}
const user = createPrivateProxy(
{ name: '张三', _password: '123456', _token: 'abc123' },
['_password', '_token'] // 这两个是私有的
);
console.log(user.name); // 张三
console.log(user._password); // 报错:属性 _password 是私有的,无法访问
user.name = '李四'; // 成功
user._password = '654321'; // 报错
console.log(Object.keys(user)); // ['name'],看不到私有属性
这个实现虽然不如真正的私有属性(#prop)安全(因为还是有办法绕过),但在很多场景下够用了,而且兼容性好。
has拦截,in操作符也能被你拿捏
has陷阱拦截in操作符。比如prop in obj,就会触发has陷阱。这个用得相对较少,但有个场景特别有用:隐藏某些属性,让in操作符检测不到。
function hideProperties(target, hiddenKeys) {
const hiddenSet = new Set(hiddenKeys);
return new Proxy(target, {
has(target, prop) {
if (hiddenSet.has(prop)) {
return false; // 假装没有
}
return prop in target;
}
});
}
const user = hideProperties(
{ name: '张三', password: '123456', secret: 'xxx' },
['password', 'secret']
);
console.log('name' in user); // true
console.log('password' in user); // false,明明有,但检测不到
console.log('secret' in user); // false
这个配合前面的私有属性实现,可以做出更完善的封装。
deleteProperty拦截,删属性前先得问问我
deleteProperty陷阱拦截delete操作。可以阻止删除某些重要属性,或者删除时做一些清理工作。
function createProtectedProxy(target, protectedKeys) {
const protectedSet = new Set(protectedKeys);
return new Proxy(target, {
deleteProperty(target, prop) {
if (protectedSet.has(prop)) {
console.error(`属性 ${prop} 受保护,无法删除`);
return false;
}
console.log(`正在删除属性:${prop}`);
delete target[prop];
return true;
}
});
}
const config = createProtectedProxy(
{ apiUrl: 'https://api.example.com', apiKey: 'xxx', debug: true },
['apiUrl', 'apiKey'] // 这两个不能删
);
delete config.debug; // 成功,打印"正在删除属性:debug"
delete config.apiKey; // 失败,打印"属性 apiKey 受保护,无法删除"
ownKeys拦截,遍历对象时的小心思
ownKeys陷阱拦截Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()、for…in循环等遍历操作。
这个陷阱返回一个数组,数组里的东西就是遍历能拿到的属性。你可以在这里过滤掉某些属性,或者添加一些虚拟属性。
function createSortedProxy(target) {
return new Proxy(target, {
ownKeys(target) {
// 返回排序后的键数组
return Object.keys(target).sort();
}
});
}
const obj = createSortedProxy({ c: 1, a: 3, b: 2 });
console.log(Object.keys(obj)); // ['a', 'b', 'c'],自动排序了
再来个实用的,给对象添加符号属性,但遍历时看不到:
function withHiddenMeta(target, meta) {
// 把元数据存到符号属性里
const metaKey = Symbol('meta');
target[metaKey] = meta;
return new Proxy(target, {
ownKeys(target) {
// 过滤掉所有符号属性
return Object.keys(target);
}
});
}
const user = withHiddenMeta(
{ name: '张三', age: 25 },
{ createdAt: Date.now(), updatedAt: Date.now() }
);
console.log(Object.keys(user)); // ['name', 'age'],看不到Symbol属性
console.log(Object.getOwnPropertySymbols(user)); // 还是能拿到,但一般代码不会这么写
apply和construct拦截,函数调用和new操作也不放过
前面讲的都是对象的拦截,Proxy还能代理函数!用apply陷阱拦截函数调用,用construct陷阱拦截new操作。
来看个函数参数校验的例子:
function validateParams(fn, validators) {
return new Proxy(fn, {
apply(target, thisArg, args) {
// 校验每个参数
args.forEach((arg, index) => {
if (validators[index] && !validators[index](arg)) {
throw new Error(`第${index + 1}个参数校验失败:${arg}`);
}
});
// 执行原函数
return target.apply(thisArg, args);
}
});
}
// 使用
const add = validateParams(
(a, b) => a + b,
[
val => typeof val === 'number', // 第一个参数必须是数字
val => typeof val === 'number' // 第二个参数必须是数字
]
);
console.log(add(1, 2)); // 3
console.log(add('1', 2)); // 报错:第1个参数校验失败:1
再来个更骚的,实现函数调用的次数限制,比如某些API有调用次数限制:
function rateLimit(fn, maxCalls, timeWindow) {
let calls = [];
return new Proxy(fn, {
apply(target, thisArg, args) {
const now = Date.now();
// 清理过期的调用记录
calls = calls.filter(time => now - time < timeWindow);
if (calls.length >= maxCalls) {
throw new Error(`调用太频繁,请${Math.ceil((calls[0] + timeWindow - now) / 1000)}秒后再试`);
}
calls.push(now);
return target.apply(thisArg, args);
}
});
}
// 限制每秒最多调用3次
const limitedLog = rateLimit(console.log, 3, 1000);
limitedLog(1); // 成功
limitedLog(2); // 成功
limitedLog(3); // 成功
limitedLog(4); // 报错:调用太频繁...
construct拦截用得少一些,主要是用来拦截new操作。比如你想实现一个单例模式:
function singleton(Class) {
let instance = null;
return new Proxy(Class, {
construct(target, args) {
if (!instance) {
instance = new target(...args);
}
return instance;
}
});
}
class Database {
constructor() {
console.log('数据库连接已创建');
}
}
const SingletonDB = singleton(Database);
const db1 = new SingletonDB(); // 数据库连接已创建
const db2 = new SingletonDB(); // 不再打印,返回同一个实例
console.log(db1 === db2); // true
其他冷门但有用的拦截器一网打尽
还有一些不常用但在特定场景很有用的陷阱:
defineProperty陷阱:拦截Object.defineProperty()。可以用来阻止别人给你的对象添加新属性。
const sealed = new Proxy({}, {
defineProperty(target, prop, descriptor) {
throw new Error('禁止添加新属性');
}
});
Object.defineProperty(sealed, 'x', { value: 1 }); // 报错
getPrototypeOf和setPrototypeOf陷阱:拦截对象原型相关操作。可以用来防止原型链污染攻击。
const safeObj = new Proxy({}, {
setPrototypeOf(target, proto) {
throw new Error('禁止修改原型');
}
});
Object.setPrototypeOf(safeObj, Array.prototype); // 报错
preventExtensions和isExtensible陷阱:拦截对象扩展相关操作。
const alwaysExtensible = new Proxy({}, {
preventExtensions(target) {
return false; // 永远不允许变成不可扩展
}
});
Object.preventExtensions(alwaysExtensible); // 失败(静默失败或报错,取决于严格模式)
这玩意儿好用但也有坑
Proxy确实强大,但也不是银弹。用之前得了解它的局限性,不然生产环境翻车了就尴尬了。
Proxy相比Object.defineProperty牛在哪
咱们来做个对比表,一目了然:
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 拦截范围 | 只能拦截已存在的属性 | 拦截整个对象的所有操作 |
| 动态属性 | 监听不到 | 完美监听 |
| 数组操作 | 需要重写数组方法 | 原生支持 |
| 删除属性 | 监听不到 | 可以监听 |
| 性能 | 初始化慢,运行时快 | 初始化快,运行时有开销 |
| 兼容性 | IE9+ | IE不支持,现代浏览器都支持 |
| 嵌套对象 | 需要递归处理 | 需要递归代理 |
总结就是:Proxy在功能上完胜,但性能和兼容性稍差。
性能方面会不会拖后腿
Proxy是有性能开销的。每次操作都要经过handler,这比直接操作对象慢。具体慢多少?看场景。
如果只是简单的get/set拦截,现代浏览器优化得不错,开销很小。但如果你在每个trap里做复杂的计算,那肯定会慢。
有个测试数据(仅供参考,不同浏览器差异很大):直接属性访问可能需要1纳秒,Proxy拦截可能需要10-50纳秒。对于普通应用,这个差别感知不到。但对于高频操作(比如动画循环里频繁访问),就要注意了。
优化建议:
- 不要在trap里做耗时操作
- 如果某些操作不需要拦截,直接返回Reflect的结果,不要额外处理
- 对于只读的大量数据,不要用Proxy,直接冻结对象(Object.freeze)性能更好
兼容性这个老问题怎么破
Proxy的兼容性是个硬伤。IE全系列不支持,包括IE11。如果你的项目需要兼容IE,那Proxy基本告别。
但对于现代项目,特别是内部系统、移动端H5、Electron桌面应用,Proxy完全没问题。Chrome 49+、Firefox 18+、Safari 10+、Edge 12+都支持。
如果一定要兼容旧浏览器,可以用polyfill,但功能有限,只能模拟部分特性。或者干脆用Object.defineProperty做降级方案。
哪些场景用了反而适得其反
不是所有场景都适合用Proxy:
简单对象:如果你只是需要一个普通对象存点数据,加Proxy就是画蛇添足,增加复杂度还降低性能。
高频数学计算:比如游戏引擎里的向量运算,每帧要计算几千次,用Proxy拦截就是作死。
需要序列化的场景:Proxy代理的对象,JSON.stringify可以正常工作(因为会触发get陷阱拿到所有属性),但如果你的trap返回了特殊值,可能会序列化出意料之外的结果。而且反序列化后,Proxy就没了,变回普通对象。
深拷贝问题:对Proxy对象做深拷贝,得到的是普通对象,Proxy的拦截逻辑全丢了。如果你依赖Proxy实现某些功能(比如私有属性),深拷贝后这些功能就失效了。
生产环境到底敢不敢上
敢!但要看场景。
Vue3已经在生产环境大规模使用Proxy了,证明它是可靠的。但Vue3团队也做了很多优化和边界情况处理。
对于你的业务代码,建议:
- 封装好,不要到处直接用new Proxy,而是封装成工具函数(比如前面的reactive、createValidator等)
- 做好边界测试,特别是this指向、嵌套对象、循环引用这些场景
- 监控性能,如果发现某个Proxy成为瓶颈,考虑优化或换方案
真实项目里我是这么用的
光讲理论没意思,来看我在真实项目里是怎么用Proxy解决实际问题的。
实现一个简易版响应式数据绑定
前面已经写了一个基础的reactive,现在咱们完善一下,加上computed和watch,更接近Vue3的体验:
// 依赖桶
const bucket = new WeakMap();
let activeEffect = null;
const effectStack = []; // 处理嵌套effect
function effect(fn, options = {}) {
const effectFn = () => {
// 清理之前的依赖
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
// 执行副作用函数,触发依赖收集
const res = fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
return res;
};
// 挂载选项,比如scheduler
effectFn.options = options;
// 存储所有依赖这个effect的依赖集合
effectFn.deps = [];
// 如果不是懒执行,立即执行
if (!options.lazy) {
effectFn();
}
return effectFn;
}
function cleanup(effectFn) {
// 从所有依赖集合中删除这个effect
effectFn.deps.forEach(deps => {
deps.delete(effectFn);
});
effectFn.deps.length = 0;
}
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
// 复制一份再执行,避免循环执行导致的无限循环
const depsToRun = new Set(deps);
depsToRun.forEach(effectFn => {
// 如果有调度器,用调度器执行
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
}
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key);
const result = Reflect.get(target, key, receiver);
if (result !== null && typeof result === 'object') {
return reactive(result);
}
return result;
},
set(target, key, newVal, receiver) {
const oldVal = target[key];
const result = Reflect.set(target, key, newVal, receiver);
if (oldVal !== newVal) {
trigger(target, key);
}
return result;
}
});
}
// 计算属性
function computed(getter) {
let value;
let dirty = true; // 脏标志,表示需要重新计算
const effectFn = effect(getter, {
lazy: true,
scheduler() {
if (!dirty) {
dirty = true;
// 触发依赖这个计算属性的effect
trigger(obj, 'value');
}
}
});
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false;
}
// 收集依赖
track(obj, 'value');
return value;
}
};
return obj;
}
// 侦听器
function watch(source, callback, options = {}) {
let getter;
if (typeof source === 'function') {
getter = source;
} else {
getter = () => traverse(source);
}
let oldValue, newValue;
const job = () => {
newValue = effectFn();
callback(newValue, oldValue);
oldValue = newValue;
};
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: job
});
if (options.immediate) {
job();
} else {
oldValue = effectFn();
}
}
// 递归遍历对象,触发所有属性的get
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) {
return;
}
seen.add(value);
for (const key in value) {
traverse(value[key], seen);
}
return value;
}
// 测试
const state = reactive({ firstName: '张', lastName: '三', age: 25 });
const fullName = computed(() => {
console.log('计算fullName');
return state.firstName + state.lastName;
});
effect(() => {
console.log('effect执行:', fullName.value);
});
console.log(fullName.value); // 计算并输出"张三"
console.log(fullName.value); // 直接输出"张三",不重新计算
state.firstName = '李'; // 触发effect重新执行
这个实现虽然比Vue3源码简单很多,但核心原理是一样的。你可以看到Proxy在这里起到了关键作用,没有Proxy,实现这么优雅的响应式系统几乎不可能。
接口请求数据自动加loading状态
做过管理后台的都知道,每个表格、每个表单的提交都要加loading状态,代码写多了烦死人。用Proxy可以自动化:
function createApiWithLoading(apiFunctions) {
return new Proxy(apiFunctions, {
get(target, prop) {
const originalFn = target[prop];
if (typeof originalFn !== 'function') {
return originalFn;
}
// 返回包装后的函数
return async function(...args) {
// 这里可以触发全局loading开始
console.log(`API ${prop} 开始请求,loading开启`);
document.body.style.cursor = 'wait';
try {
const result = await originalFn.apply(this, args);
console.log(`API ${prop} 请求成功`);
return result;
} catch (error) {
console.error(`API ${prop} 请求失败:`, error);
throw error;
} finally {
// 关闭loading
console.log(`API ${prop} 结束,loading关闭`);
document.body.style.cursor = 'default';
}
};
}
});
}
// 使用
const api = createApiWithLoading({
async getUserList() {
// 模拟请求
await new Promise(resolve => setTimeout(resolve, 1000));
return [{ id: 1, name: '张三' }];
},
async saveUser(data) {
await new Promise(resolve => setTimeout(resolve, 500));
return { success: true };
}
});
// 调用
await api.getUserList(); // 自动有loading
await api.saveUser({ name: '李四' }); // 自动有loading
这个例子比较简单,实际项目中你可以结合UI框架的loading组件,或者把loading状态绑定到Vue/React的状态管理上。
表单数据双向绑定不用框架也能搞
不想用Vue/React,只想写原生JS,但想要双向绑定?Proxy安排:
function createFormBinding(formElement, initialData) {
const data = reactive({ ...initialData });
// 绑定input事件到所有表单元素
formElement.querySelectorAll('input, select, textarea').forEach(input => {
const key = input.name;
if (!key) return;
// 初始化值
if (data[key] !== undefined) {
input.value = data[key];
}
// 监听输入,更新数据
input.addEventListener('input', (e) => {
data[key] = e.target.value;
});
// 监听数据变化,更新视图
effect(() => {
input.value = data[key];
});
});
return data;
}
// HTML结构
/*
<form id="myForm">
<input name="username" placeholder="用户名">
<input name="email" type="email" placeholder="邮箱">
<select name="gender">
<option value="male">男</option>
<option value="female">女</option>
</select>
</form>
*/
// JS
const formData = createFormBinding(document.getElementById('myForm'), {
username: '',
email: '',
gender: 'male'
});
// 监听特定字段变化
effect(() => {
console.log('用户名变为:', formData.username);
});
// 程序里修改数据,视图自动更新
setTimeout(() => {
formData.username = '自动填充的用户名';
}, 2000);
这个小工具函数可以让你在不引入大型框架的情况下,也能享受双向绑定的便利。当然,功能比较简单,复杂表单还是建议用成熟的框架。
权限校验拦截,没权限直接给你拦下来
在复杂系统中,不同角色能看到的数据、能操作的按钮都不一样。可以在数据层用Proxy做权限控制:
function withPermissionCheck(data, permissions, currentRole) {
return new Proxy(data, {
get(target, prop) {
// 检查是否有读权限
const requiredPermission = permissions[prop]?.read;
if (requiredPermission && !currentRole.includes(requiredPermission)) {
console.warn(`角色 ${currentRole} 无权限读取属性 ${prop}`);
return undefined; // 或者返回'***'之类的占位符
}
return target[prop];
},
set(target, prop, value) {
const requiredPermission = permissions[prop]?.write;
if (requiredPermission && !currentRole.includes(requiredPermission)) {
console.error(`角色 ${currentRole} 无权限修改属性 ${prop}`);
return false;
}
target[prop] = value;
return true;
}
});
}
// 使用
const userData = {
name: '张三',
salary: 50000,
idCard: '11010119900101xxxx'
};
const permissions = {
name: { read: 'user:read', write: 'user:write' },
salary: { read: 'salary:read', write: 'salary:write' },
idCard: { read: 'sensitive:read', write: 'sensitive:write' }
};
const adminView = withPermissionCheck(userData, permissions, ['user:read', 'user:write', 'salary:read', 'salary:write', 'sensitive:read']);
const hrView = withPermissionCheck(userData, permissions, ['user:read', 'salary:read']);
const normalView = withPermissionCheck(userData, permissions, ['user:read']);
console.log(adminView.salary); // 50000
console.log(hrView.salary); // 50000
console.log(normalView.salary); // undefined,并打印警告
adminView.salary = 60000; // 成功
hrView.salary = 60000; // 失败,无写权限
这种方式把权限控制下沉到数据层,业务代码里不用到处写if判断,代码干净很多。而且权限变更时,只需要改配置,不用改业务代码。
日志埋点自动化,不用到处手动写
埋点代码散落在业务代码里,维护起来头疼。用Proxy可以自动拦截关键操作并上报:
function withAutoTracking(obj, eventName, tracker) {
return new Proxy(obj, {
get(target, prop) {
const value = target[prop];
// 如果属性是函数,包装一下
if (typeof value === 'function') {
return function(...args) {
// 上报函数调用
tracker.track(eventName, {
action: 'call',
method: prop,
args: args.map(arg => typeof arg === 'object' ? '[Object]' : arg),
timestamp: Date.now()
});
return value.apply(this, args);
};
}
// 上报属性访问
tracker.track(eventName, {
action: 'get',
property: prop,
timestamp: Date.now()
});
return value;
},
set(target, prop, value) {
// 上报属性修改
tracker.track(eventName, {
action: 'set',
property: prop,
newValue: typeof value === 'object' ? '[Object]' : value,
timestamp: Date.now()
});
target[prop] = value;
return true;
}
});
}
// 模拟上报器
const tracker = {
track(event, data) {
console.log(`[埋点] ${event}:`, data);
// 实际这里发送到埋点服务器
}
};
// 使用
const shopCart = withAutoTracking({
items: [],
addItem(item) {
this.items.push(item);
},
removeItem(id) {
this.items = this.items.filter(item => item.id !== id);
},
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}, 'shop_cart', tracker);
shopCart.addItem({ id: 1, name: '手机', price: 2999 }); // 自动上报
console.log(shopCart.total); // 自动上报
shopCart.items = []; // 自动上报
这种方式实现了AOP(面向切面编程),业务代码零侵入,埋点逻辑完全解耦。而且你可以灵活控制上报哪些操作、过滤哪些数据。
API代理转发,前端也能玩后端那套
开发环境经常遇到跨域问题,或者需要把请求转发到不同的后端服务。可以用Proxy实现一个简单的API网关:
function createApiGateway(config) {
return new Proxy({}, {
get(target, serviceName) {
const serviceConfig = config[serviceName];
if (!serviceConfig) {
throw new Error(`未知的服务:${serviceName}`);
}
return new Proxy({}, {
get(target, methodName) {
return async function(params) {
const url = `${serviceConfig.baseUrl}${serviceConfig.methods[methodName] || '/' + methodName}`;
console.log(`[网关] 转发请求到 ${serviceName}.${methodName}: ${url}`);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...serviceConfig.headers
},
body: JSON.stringify(params)
});
return response.json();
};
}
});
}
});
}
// 配置
const api = createApiGateway({
userService: {
baseUrl: 'http://localhost:3001',
headers: { 'X-Service': 'user' },
methods: {
getProfile: '/profile',
updateSettings: '/settings'
}
},
orderService: {
baseUrl: 'http://localhost:3002',
headers: { 'X-Service': 'order' },
methods: {
create: '/orders',
list: '/orders/list'
}
}
});
// 使用
const profile = await api.userService.getProfile({ userId: 123 });
const orders = await api.orderService.list({ page: 1, size: 10 });
这个例子展示了Proxy的嵌套使用,通过两层代理,实现了类似后端RPC的调用体验。实际项目中可以加上错误处理、重试机制、缓存等功能。
踩坑实录和救急方案
Proxy好用,但坑也不少。下面这些都是我踩过的坑,血淋淋的教训。
Proxy代理后this指向乱了怎么修
这是最常见的问题。看代码:
const target = {
name: '张三',
getName() {
return this.name;
}
};
const proxy = new Proxy(target, {
get(target, prop, receiver) {
return target[prop];
}
});
console.log(target.getName()); // 张三
console.log(proxy.getName()); // undefined,卧槽?
为啥proxy.getName()返回undefined?因为getName里的this指向的是proxy,而proxy上没有name属性(name在target上)。
解决办法是用Reflect.get,并把receiver传进去:
const proxy = new Proxy(target, {
get(target, prop, receiver) {
const value = target[prop];
// 如果值是函数,绑定this
if (typeof value === 'function') {
return value.bind(target); // 绑定到target
}
return value;
}
});
console.log(proxy.getName()); // 张三
或者用Reflect:
const proxy = new Proxy(target, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
});
Reflect.get的第三个参数receiver会作为getter执行时的this值。这样如果属性是getter函数,里面的this就指向receiver(也就是proxy),而proxy代理了target,所以能正确访问到name。
但注意,如果是普通函数(不是getter),Reflect.get不会自动绑定this,你还是需要手动bind。
嵌套对象代理失效的尴尬场面
前面写的reactive函数有个问题:嵌套对象的代理在重新赋值后会失效。
const state = reactive({
user: {
name: '张三'
}
});
effect(() => {
console.log(state.user.name);
});
state.user.name = '李四'; // 能触发更新
state.user = { name: '王五' }; // 也能触发更新
state.user.name = '赵六'; // 等等,这次没触发?!
为啥最后一次没触发?因为state.user被重新赋值为一个新对象{ name: ‘王五’ },这个新对象是普通对象,不是响应式的!
解决办法是在set的时候也做递归代理:
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key);
if (result !== null && typeof result === 'object') {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
// 如果新值是对象,先转成响应式
if (value !== null && typeof value === 'object' && !value.__isReactive) {
value = reactive(value);
value.__isReactive = true; // 标记一下,避免重复代理
}
const oldVal = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldVal !== value) {
trigger(target, key);
}
return result;
}
});
}
Vue3里用了WeakMap来缓存已经代理过的对象,避免重复代理,性能更好。
循环引用导致栈溢出的血泪教训
如果对象有循环引用,递归代理会导致栈溢出:
const obj = { name: 'test' };
obj.self = obj; // 循环引用
const proxy = reactive(obj); // RangeError: Maximum call stack size exceeded
解决办法是用WeakMap缓存已经代理过的对象:
const reactiveMap = new WeakMap();
function reactive(target) {
// 如果已经代理过,直接返回
if (reactiveMap.has(target)) {
return reactiveMap.get(target);
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key);
if (result !== null && typeof result === 'object') {
return reactive(result);
}
return result;
},
// ... set等其他陷阱
});
reactiveMap.set(target, proxy);
return proxy;
}
这样遇到循环引用时,直接返回缓存的代理对象,不会无限递归。
调试时候看不到内部属性的烦恼
Proxy代理的对象,在控制台打印出来,有时候看不到内部细节,或者看到的内容和实际不符(因为get陷阱可能返回了计算值)。
解决办法:
- 用console.log(JSON.parse(JSON.stringify(proxy)))看原始数据
- 在handler里加debugger断点
- 用Reflect.ownKeys(proxy)看所有属性
Chrome DevTools现在也支持Proxy的调试了,可以在Scope面板看到Proxy的target和handler。
和TypeScript一起用时的类型丢失问题
TypeScript对Proxy的支持不太好,代理后的对象类型会丢失,或者类型推断不正确。
interface User {
name: string;
age: number;
}
const user: User = { name: '张三', age: 25 };
const proxy = new Proxy(user, {});
// proxy的类型是User,但实际上它可能返回Proxy返回的任何值
// TypeScript无法知道get陷阱做了什么
解决办法是用类型断言,或者定义复杂的泛型来推断Proxy返回的类型。但说实话,都挺麻烦的。实际项目中,建议把Proxy封装在内部,对外暴露的接口保持正常类型。
function createReactive<T extends object>(target: T): T {
return new Proxy(target, {}) as T; // 类型断言
}
遇到报错别慌,按这个思路一步步排查
Proxy相关的bug排查思路:
- 先确定是Proxy的问题:把Proxy去掉,直接操作原对象,看是否正常
- 检查handler的返回值:get必须返回属性值,set必须返回布尔值,很多人忘记return
- 检查this指向:特别是方法调用时,this可能不是预期的对象
- 检查receiver参数:用Reflect时记得传receiver
- 检查递归代理:嵌套对象是否都代理了?循环引用处理了没?
- 检查trap的触发时机:不是所有操作都会触发你预期的trap,查MDN确认
老前端私藏的几个骚操作
用Proxy实现链式调用爽到飞起
jQuery的链式调用很爽,但现代框架很少用了。用Proxy可以实现更强大的链式操作:
function createChainable(obj) {
return new Proxy(obj, {
get(target, prop) {
if (prop in target) {
const value = target[prop];
// 如果属性是函数,返回包装后的函数,保持链式
if (typeof value === 'function') {
return function(...args) {
const result = value.apply(this, args);
// 如果返回值是undefined,返回proxy保持链式;否则返回结果
return result === undefined ? this : result;
};
}
return value;
}
// 如果属性不存在,返回一个空函数,什么都不做但保持链式
return () => this;
}
});
}
// 使用
const calculator = createChainable({
value: 0,
add(n) {
this.value += n;
},
subtract(n) {
this.value -= n;
},
multiply(n) {
this.value *= n;
},
getValue() {
return this.value;
}
});
const result = calculator.add(5).subtract(2).multiply(3).getValue();
console.log(result); // 9
虚拟列表数据懒加载就这么简单
大数据列表渲染卡顿?用Proxy实现虚拟列表的数据懒加载:
function createVirtualList(totalCount, itemHeight, containerHeight) {
const visibleCount = Math.ceil(containerHeight / itemHeight) + 2; // 多渲染2个缓冲
return new Proxy({}, {
get(target, index) {
index = parseInt(index);
if (isNaN(index) || index < 0 || index >= totalCount) {
return undefined;
}
// 只加载可见区域的数据
if (!target[index]) {
console.log(`懒加载第${index}条数据`);
target[index] = {
id: index,
content: `内容${index}`,
loadedAt: Date.now()
};
}
return target[index];
},
has(target, index) {
return parseInt(index) >= 0 && parseInt(index) < totalCount;
},
ownKeys(target) {
// 返回所有可能的索引
return Array.from({ length: totalCount }, (_, i) => String(i));
},
getOwnPropertyDescriptor(target, index) {
return {
enumerable: true,
configurable: true
};
}
});
}
// 使用
const list = createVirtualList(100000, 50, 500); // 10万条数据,每项50px,容器500px
// 只加载了可见区域的数据
console.log(list[0]); // 有数据,懒加载
console.log(list[1]); // 有数据,懒加载
console.log(list[100]); // undefined,还没加载,但has返回true
模仿Vue3的reactive自己造轮子
前面已经写过了,这里再给个更完整的版本,支持数组方法:
const reactiveMap = new WeakMap();
const rawMap = new WeakMap(); // 反向查找,从proxy找原对象
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
if (reactiveMap.has(target)) {
return reactiveMap.get(target);
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (key === '__isReactive') return true;
if (key === '__raw') return target;
const result = Reflect.get(target, key, receiver);
track(target, key);
if (result !== null && typeof result === 'object') {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
// 如果新值已经是响应式,取原对象
if (value && value.__isReactive) {
value = value.__raw;
}
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = delete target[key];
if (hadKey && result) {
trigger(target, key);
}
return result;
},
has(target, key) {
track(target, key);
return Reflect.has(target, key);
},
ownKeys(target) {
track(target, '__iterate__');
return Reflect.ownKeys(target);
}
});
reactiveMap.set(target, proxy);
rawMap.set(proxy, target);
return proxy;
}
function toRaw(proxy) {
return proxy && proxy.__raw ? proxy.__raw : proxy;
}
// 数组方法重写
const arrayInstrumentations = {};
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
const original = Array.prototype[method];
arrayInstrumentations[method] = function(...args) {
// 暂停依赖收集,避免无限循环
pauseTracking();
const result = original.apply(this, args);
resumeTracking();
return result;
};
});
接口数据自动转换驼峰和下划线
前面写过getter版本的,这里写个更完善的,支持双向转换:
function createNamingProxy(data, type = 'camel') {
const cache = new Map();
// 转换函数
const convert = type === 'camel' ? toCamelCase : toSnakeCase;
function convertKey(key) {
if (!cache.has(key)) {
cache.set(key, convert(key));
}
return cache.get(key);
}
return new Proxy(data, {
get(target, key) {
// 找到实际的key
const actualKey = findActualKey(target, key, type);
if (!actualKey) return undefined;
const value = target[actualKey];
// 如果是对象,递归代理
if (value !== null && typeof value === 'object') {
return createNamingProxy(value, type);
}
return value;
},
set(target, key, value) {
const actualKey = findActualKey(target, key, type) || (type === 'camel' ? toSnakeCase(key) : key);
target[actualKey] = value;
return true;
},
ownKeys(target) {
return Object.keys(target).map(key =>
type === 'camel' ? toCamelCase(key) : toSnakeCase(key)
);
},
getOwnPropertyDescriptor(target, key) {
const actualKey = findActualKey(target, key, type);
if (!actualKey) return undefined;
return Object.getOwnPropertyDescriptor(target, actualKey);
}
});
}
function findActualKey(target, key, type) {
const keys = Object.keys(target);
// 先找直接匹配的
if (keys.includes(key)) return key;
// 再找转换后的
const converted = type === 'camel' ? toSnakeCase(key) : toCamelCase(key);
if (keys.includes(converted)) return converted;
return undefined;
}
function toCamelCase(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
function toSnakeCase(str) {
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
}
// 使用
const serverData = {
user_name: '张三',
user_info: {
phone_number: '13800138000',
email_addr: 'test@example.com'
}
};
const user = createNamingProxy(serverData, 'camel');
console.log(user.userName); // 张三
console.log(user.userInfo.phoneNumber); // 13800138000
user.userName = '李四'; // 实际修改的是user_name
防抖节流包装成Proxy一行搞定
把防抖节流逻辑封装成Proxy,可以自动应用到对象的所有方法上:
function withDebounce(obj, delay, immediate = false) {
const timers = new Map();
return new Proxy(obj, {
get(target, prop) {
const value = target[prop];
if (typeof value !== 'function') {
return value;
}
return function(...args) {
const context = this;
if (timers.has(prop)) {
clearTimeout(timers.get(prop));
}
const timer = setTimeout(() => {
timers.delete(prop);
if (!immediate) {
value.apply(context, args);
}
}, delay);
timers.set(prop, timer);
if (immediate && !timers.has(prop)) {
value.apply(context, args);
}
};
}
});
}
function withThrottle(obj, limit) {
let inThrottle = new Map();
return new Proxy(obj, {
get(target, prop) {
const value = target[prop];
if (typeof value !== 'function') {
return value;
}
return function(...args) {
const context = this;
if (!inThrottle.get(prop)) {
value.apply(context, args);
inThrottle.set(prop, true);
setTimeout(() => inThrottle.set(prop, false), limit);
}
};
}
});
}
// 使用
const searchAPI = withDebounce({
async search(keyword) {
console.log(`搜索:${keyword}`);
// 实际发送请求...
}
}, 300);
// 快速输入时只会在停止输入300ms后执行一次
searchAPI.search('a');
searchAPI.search('ab');
searchAPI.search('abc'); // 只有这次会执行
测试 mock 数据不用到处改代码
开发时经常需要mock数据,发布时又要改回真实接口。用Proxy可以实现无缝切换:
const IS_MOCK = process.env.NODE_ENV === 'development';
function createApiClient(realAPI, mockAPI) {
if (!IS_MOCK) {
return realAPI;
}
return new Proxy(realAPI, {
get(target, prop) {
if (mockAPI[prop]) {
console.log(`[Mock] 使用mock数据:${prop}`);
return mockAPI[prop];
}
console.log(`[Mock] 没有mock数据,使用真实接口:${prop}`);
return target[prop];
}
});
}
// 真实API
const realAPI = {
async getUser() {
const res = await fetch('/api/user');
return res.json();
}
};
// Mock数据
const mockAPI = {
async getUser() {
return { id: 1, name: 'Mock用户', mock: true };
}
};
const api = createApiClient(realAPI, mockAPI);
const user = await api.getUser(); // 开发时返回mock数据,生产环境返回真实数据
最后唠两句掏心窝子的话
写到这儿,估计你也看累了。说实话,Proxy这东西,刚学的时候觉得"哇好牛逼",用的时候觉得"卧槽怎么又出bug了",用熟了之后觉得"真香,离不开了"。
技术这东西就是这样,没有银弹。Proxy很强,但不是万能的。简单场景别硬上,复杂场景别害怕。多写代码多踩坑,踩多了你就成大神了。
还有啊,看完这篇文章别收藏了就完事儿,动手敲一遍代码。你看我写了这么多代码示例,你不跑一遍,根本不知道自己哪里没理解。最好是看完一个例子,自己不看代码重新写一遍,写不出来再回头看,这样记忆最深。
遇到不懂的,MDN文档是你的好朋友,虽然写得有点枯燥,但最准确。也可以去翻翻Vue3的源码,看看尤大是怎么用Proxy的,那代码写得是真漂亮。
行了,废话不多说,赶紧打开VS Code开始敲代码吧。有啥问题,欢迎交流,咱们一起踩坑一起进步。
(全文完,大概6000多字,手都写酸了,希望能帮到你)

更多推荐



所有评论(0)