的,大纲收到了!这大纲写得挺实在的,"唠唠"、"坑过的惨痛经历"、"老铁"这些词儿一看就不是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纳秒。对于普通应用,这个差别感知不到。但对于高频操作(比如动画循环里频繁访问),就要注意了。

优化建议:

  1. 不要在trap里做耗时操作
  2. 如果某些操作不需要拦截,直接返回Reflect的结果,不要额外处理
  3. 对于只读的大量数据,不要用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团队也做了很多优化和边界情况处理。

对于你的业务代码,建议:

  1. 封装好,不要到处直接用new Proxy,而是封装成工具函数(比如前面的reactive、createValidator等)
  2. 做好边界测试,特别是this指向、嵌套对象、循环引用这些场景
  3. 监控性能,如果发现某个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陷阱可能返回了计算值)。

解决办法:

  1. 用console.log(JSON.parse(JSON.stringify(proxy)))看原始数据
  2. 在handler里加debugger断点
  3. 用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排查思路:

  1. 先确定是Proxy的问题:把Proxy去掉,直接操作原对象,看是否正常
  2. 检查handler的返回值:get必须返回属性值,set必须返回布尔值,很多人忘记return
  3. 检查this指向:特别是方法调用时,this可能不是预期的对象
  4. 检查receiver参数:用Reflect时记得传receiver
  5. 检查递归代理:嵌套对象是否都代理了?循环引用处理了没?
  6. 检查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多字,手都写酸了,希望能帮到你)

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐