JavaScript Proxy 详解:从基础用法到高级应用场景

作者:闭包旅人时间:2026-10-24 11:10:44浏览:82

本文深入解析 JavaScript 中的 Proxy 对象,涵盖其基本结构、13种拦截方法详解,以及与 Reflect 对象的配合使用。通过数组代理、函数调用统计、防抖实现及观察者模式等实战案例,展示 Proxy 在现代前端框架(如 Vue3、Mobx)中的核心作用,并对比其与 Object.defineProperty 的优劣。

1. Proxy 的基础构造

Proxy 的核心用法如下:


/**
 * target: 表示要代理的目标,可以是object, array, function类型
 * handler: 是一个对象,可以编写各种代理的方法
 */
const proxy = new Proxy(target, handler);

假设我们需要对某个对象进行代理,可以通过定义 get 和 set 方法来拦截属性的读取与赋值行为:


const person = {
  name: 'wenzi',
  age: 20,
};
const personProxy = new Proxy(person, {
  get(target, key, receiver) {
    console.log(`get value by ${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`set ${key}, old value ${target[key]} to ${value}`);
    target[key] = value;
  },
});

Proxy 本质上仅作为代理层存在,personProxy 继承了 person 的所有属性和方法。当通过 personProxy 访问或修改 name 属性时,会触发相应的日志记录:


personProxy.name; // "wenzi"
// log: get value by name

personProxy.name = 'hello';
// log: set name, old value wenzi to hello

同时,通过 personProxy 修改数据时,底层原对象的数据也会同步更新。打印 person 对象后,可以看到 name 字段的值已更新为 hello:


console.log(person); // {name: "hello", age: 20}

Proxy 的第二个参数 handler 除了支持 get 和 set 外,还包含多种其他拦截方法:

1.get(target, propKey, receiver):拦截对象属性的读取操作,例如 proxy.foo 或 proxy['foo']。

2.set(target, propKey, value, receiver):拦截对象属性的赋值操作,例如 proxy.foo = v 或 proxy['foo'] = v,该方法返回布尔值。

3.has(target, propKey):拦截 propKey in proxy 的检查操作,返回布尔值。

4.deleteProperty(target, propKey):拦截 delete proxy[propKey] 的删除操作,返回布尔值。

5.ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy) 以及 for...in 循环,返回数组。此方法返回目标对象所有自身属性的名称,而 Object.keys() 仅返回可遍历的自身属性。

6.getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

7.defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc) 和 Object.defineProperties(proxy, propDescs),返回布尔值。

8.preventExtensions(target):拦截 Object.preventExtensions(proxy),返回布尔值。

9.getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回对象。

10.isExtensible(target):拦截 Object.isExtensible(proxy),返回布尔值。

11.setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回布尔值。若目标对象为函数,则额外支持两种拦截操作。

12.apply(target, object, args):拦截 Proxy 实例作为函数调用的行为,例如 proxy(...args)、proxy.call(object, ...args) 或 proxy.apply(...)。

13.construct(target, args):拦截 Proxy 实例作为构造函数调用的行为,例如 new proxy(...args)。

例如,当我们使用 delete 删除元素时,可以通过 deleteProperty() 方法来拦截该操作。沿用上述代理 person 的代码,我们添加 deleteProperty 方法:


const person = {
  name: 'wenzi',
  age: 20,
};
const personProxy = new Proxy(person, {
  // 忽略get和set方法,与上面一样
  // ...
  deleteProperty(target, key, receiver) {
    console.log(`delete key ${key}`);
    delete target[key];
  },
});

当执行 delete 操作时:


delete personProxy['age'];
// log: delete key age

2. Proxy 与 Reflect 的关系

Proxy 与 Reflect 紧密相关,Reflect 中的方法及其使用方式与 Proxy 完全一致。

例如,上述 Proxy 中的 get()、set() 和 deleteProperty() 方法原本直接操作原代理对象,这里我们改为使用Reflect来执行:


const personProxy = new Proxy(person, {
  get(target, key, receiver) {
    console.log(`get value by ${key}`);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log(`set ${key}, old value ${target[key]} to ${value}`);
    return Reflect.set(target, key, value, receiver);
  },
  deleteProperty(target, key, receiver) {
    console.log(`delete key ${key}`);
    return Reflect.deleteProperty(target, key, receiver);
  },
});

可以发现,这样完美实现了上述功能。

1. Proxy 的基础构造

3. 代理数组

在之前的文章《Vue 中对数组特殊的操作》中,我们讨论过 Vue 为何未使用 Object.defineProperty 来劫持数据,而是重写了 Array 原型链上的几个方法,通过重写的方法来实现 Vue 模板中数据的更新。

但如果使用 Proxy,则可以直接代理数组:


const arr = [1, 2, 3, 4];
const arrProxy = new Proxy(arr, {
  get(target, key, receiver) {
    console.log('arrProxy.get', target, key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log('arrProxy.set', target, key, value);
    return Reflect.set(target, key, value, receiver);
  },
  deleteProperty(target, key) {
    console.log('arrProxy.deleteProperty', target, key);
    return Reflect.deleteProperty(target, key);
  },
});

现在我们来操作一下代理后的数组 arrProxy 进行观察:


arrProxy[2] = 22; // arrProxy.set (4) [1, 2, 3, 4] 2 22
arrProxy[3]; // arrProxy.get (4) [1, 2, 22, 4] 3
delete arrProxy[2]; // arrProxy.deleteProperty (4) [1, 2, 22, 4] 2
arrProxy.push(5); // push操作比较复杂,这里进行了多个get()和set()操作
arrProxy.length; // arrProxy.get (5) [1, 2, empty, 4, 5] length

可以看到,无论是获取、删除还是修改数据,Proxy 都能感知到。此外,数组原型链上的部分方法,如:

1.push()

2.pop()

3.shift()

4.unshift()

5.splice()

6.sort()

7.reverse()

也都可以通过 Proxy 中的代理方法进行劫持。

concat() 方法较为特殊,它属于赋值操作且不改变原数组,因此在调用 concat() 方法操作数组时,若无赋值操作,则仅触发 get() 拦截。

1. Proxy 的基础构造

4. 代理函数

Proxy 中还包含 apply() 方法,用于拦截函数作为函数调用时的操作。


const getSum = (...args) => {
  if (!args.every((item) => typeof item === 'number')) {
    throw new TypeError('参数应当均为number类型');
  }
  return args.reduce((sum, item) => sum + item, 0);
};
const fnProxy = new Proxy(getSum, {
  /**
   * @params {Fuction} target 代理的对象
   * @params {any} ctx 执行的上下文
   * @params {any} args 参数
   */
  apply(target, ctx, args) {
    console.log('ctx', ctx);
    console.log(`execute fn ${getSum.name}, args: ${args}`);
    return Reflect.apply(target, ctx, args);
  },
});

执行 fnProxy:


// 10, ctx为undefined, log: execute fn getSum, args: 1,2,3,4
fnProxy(1, 2, 3, 4);

// ctx为undefined, Uncaught TypeError: 参数应当均为number类型
fnProxy(1, 2, 3, '4');

// 10, ctx为window, log: execute fn getSum, args: 1,2,3,4
fnProxy.apply(window, [1, 2, 3, 4]);

// 6, ctx为window, log: execute fn getSum, args: 1,2,3
fnProxy.call(window, 1, 2, 3);

// 6, ctx为person, log: execute fn getSum, args: 1,2,3
fnProxy.apply(person, [1, 2, 3]);

5. 一些简单的应用场景

我们知道 Vue3 已使用 Proxy 重写了响应式系统,mobx 也采用了 Proxy 模式。在可见的未来,Proxy 的应用场景将更加广泛,此处我们简要介绍几个案例。

5.1 统计函数被调用的上下文和次数

此处我们利用 Proxy 代理函数,以记录函数被调用时的上下文及调用次数。


const countExecute = (fn) => {
  let count = 0;

  return new Proxy(fn, {
    apply(target, ctx, args) {
      ++count;
      console.log('ctx上下文:', ctx);
      console.log(`${fn.name} 已被调用 ${count} 次`);
      return Reflect.apply(target, ctx, args);
    },
  });
};

现在我们来代理刚才的 getSum() 方法:


const getSum = (...args) => {
  if (!args.every((item) => typeof item === 'number')) {
    throw new TypeError('参数应当均为number类型');
  }
  return args.reduce((sum, item) => sum + item, 0);
};

const useSum = countExecute(getSum);

useSum(1, 2, 3); // getSum 已被调用 1 次

useSum.apply(window, [2, 3, 4]); // getSum 已被调用 2 次

useSum.call(person, 3, 4, 5); // getSum 已被调用 3 次

5.2 实现一个防抖功能

基于上述统计函数调用次数的功能,这也为我们实现函数防抖提供了灵感。


const throttleByProxy = (fn, rate) => {
  let lastTime = 0;
  return new Proxy(fn, {
    apply(target, ctx, args) {
      const now = Date.now();
      if (now - lastTime > rate) {
        lastTime = now;
        return Reflect.apply(target, ctx, args);
      }
    },
  });
};

const logTimeStamp = () => console.log(Date.now());
window.addEventListener('scroll', throttleByProxy(logTimeStamp, 300));

logTimeStamp() 至少需要间隔 300ms 才能执行一次。

5.3 实现观察者模式

在此处,我们实现一个最简单的类似 mobx 的观察者模式。


const list = new Set();
const observe = (fn) => list.add(fn);
const observable = (obj) => {
  return new Proxy(obj, {
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      list.forEach((observer) => observer());
      return result;
    },
  });
};
const person = observable({ name: 'wenzi', age: 20 });
const App = () => {
  console.log(`App -> name: ${person.name}, age: ${person.age}`);
};
observe(App);

person 是使用 Proxy 创建的代理对象,每当 person 中的属性发生变化时,便会执行 App() 函数。由此实现了一个简单的响应式状态管理。

6. Proxy 与 Object.defineProperty 的对比

上述许多示例使用 Object.defineProperty 同样可以实现。那么这两者各自有哪些优缺点呢?

6.1 Object.defineProperty 的优劣

Object.defineProperty 的兼容性相比 Proxy 更为优秀,除极老旧的 IE6、IE7 浏览器外,其他主流浏览器均提供支持。

但 Object.defineProperty 支持的方法众多,且主要基于属性进行拦截。因此在 Vue2 中,只能通过重写 Array 原型链上的方法来操作数组。

6.2 Proxy 的优劣

Proxy 与上述情况正好相反,它是基于对象进行代理的,因此可代理更多类型,例如 Object、Array、Function 等;且支持代理的方法也更多。

其劣势在于兼容性较差,即使使用 polyfill 也无法完美实现。

7. 总结

Proxy 能实现的功能还有很多,后续我们将继续探索,并尽可能深入了解基于 Proxy 实现的类库,例如 mobx5 的源码和实现原理等。

以上就是《JS带你深入领略Proxy的世界》的详细内容,更多关于JS中的代理Proxy的资料请关注本站其它相关文章!

作者最新文章