9.1 代理基础

代理是目标对象的抽象,可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。但直接操作会绕过代理施予的行为。

9.1.1 创建空代理

代理是通过Proxy构造函数创建的。该构造函数接收两个参数:目标对象 (Target)和处理程序 (Handler)。缺少一个就会报错TypeError.

const target = { id: 'target'};
const handler = {};
const proxy = new Proxy(target, handler);

//属性访问会访问同一个值
console.log(target.id);//target
console.log(proxy.id);//target
//给目标属性赋值会反映在代理上
target.id = 'target2';
console.log(target.id);//target2
console.log(proxy.id);//target2
//给代理属性赋值会反映在目标上
proxy.id = 'proxy2';
console.log(target.id);//proxy2
console.log(proxy.id);//proxy2
//hasOwnProperty()方法在两个地方都会应用到目标对象
console.log(target.hasOwnProperty('id'));//true
console.log(proxy.hasOwnProperty('id'));//true
//Proxy.prototype是Undefined,因此不能使用instanceof操作符
console.log(proxy instanceof proxy);//报错
console.log(target instanceof proxy);//报错
//可以通过严格相等来区分代理与目标
console.log(target === proxy);//true

9.1.2 定义捕获器

使用代理的主要目的是可以定义捕获器(trap)。作用是在处理程序中定义的“基本操作的拦截器”。每个处理程序对象可以包含不定数量的捕获器,每种捕获器都对应一种基本操作,可以直接或间接在处理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

const target = {
  foo: "bar",
};
const handler = {
  get() {
    return "handler override";
  },
};
const proxy = new Proxy(target, handler);
console.log(target.foo);//bar
console.log(proxy.foo);//handler override

console.log(target['foo']);//bar
console.log(proxy['foo']);//handler override

console.log(Object.create(target)['foo']);//bar
console.log(Object.create(proxy)['foo']);//handler override

如以上代码所示,只有当通过代理对象执行get()操作时才会触发捕获器。proxy[property]、proxy.property或Object.create(proxy)[property]等操作都会触发基本的get()操作以获取属性。

9.1.3 捕获器参数和反射API

所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获的原始行为。

const target = {
  foo: "bar",
};
const handler = {
  //接收三个参数:目标对象、要查询的属性名、代理对象
  get(trapTarget, property, receiver) {
    console.log(trapTarget, property, receiver);
    console.log(trapTarget === target);
    console.log(receiver === proxy);
    //可以return自己处理后的数据
  },
};
const proxy = new Proxy(target, handler);
proxy.foo;
//{ foo: 'bar' } foo { foo: 'bar' }
//true
//true

实际上开发者可以通过调用全局对象Reflect上的同名方法轻松重建,这些方法具有与被拦截方法相同的行为:

const target = {
  foo: "bar",
};
const handler = {
  get() {
    return Reflect.get(...arguments);
  },
  /*更简洁的写法:
    get: Reflect.get
  */
};
const proxy = new Proxy(target, handler);
//也可以不定义handler,直接使用Reflect.get
//const proxy = new Proxy(target, Reflect);
console.log(target.foo);//bar
console.log(proxy.foo);//bar

实际使用示例:

const target = {
  foo: "bar",
  baz: "qux"
};
const handler = {
  get (trapTarget, property, receiver){
    let dec = '';
    if(property == 'foo'){
      dec = '---';
    }
    return Reflect.get(...arguments) + dec;
  }
};
const proxy = new Proxy(target, handler);
console.log(target.foo);//bar
console.log(proxy.foo);//bar---
console.log(target.baz);//qux
console.log(proxy.baz);//qux

9.1.4 捕获器不变式

使用捕获器几乎可以改变所有基本方法的行为,但根据ECMAScript规范,捕获处理程序必须遵循“捕获器不变式”,捕获器不变式因方法而已,但通常都是为了防止出现反常行为

如下例子:目标对象有一个不可配置不可写的数据属性,当捕获器返回一个与该属性不同的值时抛出TypeError错误。

const target = {};
Object.defineProperty(target, 'name', {
  configurable: false,
  writable: false,
  value: '张三'
})
const handler = {
  get(){
    return '李四';
  }
}
const proxy = new Proxy(target, handler);
console.log(proxy.name);
/*TypeError: 'get' on proxy: property 'name' is a read-only
 and non-configurable data property on the proxy target 
 but the proxy did not return its actual value (expected '张三' but got '李四')*/

9.1.5 可撤销代理

对于使用new Proxy()创建的普通代理来说,这种联系会在代理对象的生命周期中一直持续存在。可以通过Proxy暴露的revocable()方法撤销代理对象与目标对象的关联。撤销操作不可逆。撤销函数revoke()是幂等的,访问撤销后的代理会抛出错误

const target = {
  name: '张三'
};
const handler = {
  get(){
    return '李四';
  }
}
const {proxy, revoke} = Proxy.revocable(target, handler);
console.log(target.name);//张三
console.log(proxy.name);//李四
revoke();
console.log(proxy.name);
//ypeError: Cannot perform 'get' on a proxy that has been revoked

9.1.6 代理另一个代理

通过一个代理去代理另一个代理可以实现在一个目标对象上构建多层拦截网。

const target = {
  name: '张三'
};
const firstProxy = new Proxy(target, {
  get() {
    console.log('first proxy');
    return Reflect.get(...arguments);
  }
})
const secondProxy = new Proxy(firstProxy, {
  get() {
    console.log('second proxy');
    return Reflect.get(...arguments);
  }
})
console.log(secondProxy.name);
//second proxy
//first proxy
//张三

9.1.7 代理的问题与不足

1、代理中的this

如果目标对象依赖于对象标识,那么就可能遇到意料之外的问题:

const wm = new WeakMap();
class User {
  constructor(userId){
    wm.set(this, userId);
  }
  set id(userId){
    wm.set(this, userId);
  }
  get id(){
    return wm.get(this);
  }
}
const user = new User(123);
console.log(user.id);//123
const userInstanceProxy = new Proxy(User,{});
console.log(userInstanceProxy.id);//undefined
//需要重新配置代理,把代理实例改为代理User类本身。之后再创建代理的实例,就会以代理实例作为WeakMap的键了。
const proxyUser = new userInstanceProxy(456);
console.log(proxyUser.id);//456
2、代理与内部槽位

当有些内置类型可能依赖代理无法控制的机制,就会导致在代理上调用某些方法报错。例如Date类型:该类型方法的执行依赖this值上的内部槽位[[NumberDate]],但是代理对象上不存在这个内部槽位,且该内部槽位也无法通过get()和set()操作访问到。

const wm = new Date();
const proxy = new Proxy(wm, {});
console.log(proxy instanceof Date);//true
proxy.getDate();//TypeError: this is not a Date object.

9.2 代理捕获器与反射方法

代理可以捕获13种不同的基本操作,它们各自有着不同的反射API、参数、关联ECMAScript操作和不变式。

9.2.1 get()

拦截的操作:属性读取、对应的反射API:Reflect.get()

返回值:无限制
触发场景

  • proxy.property
  • proxy[property]
  • Object.create(proxy)[property]
  • Reflect.get(proxy, property, receiver)

参数:

  • target: 目标对象。
  • property: 引用的目标对象上的键属性
  • receiver: 代理对象或继承代理对象的对象
const handler = {
  get(target, property, receiver) {
    console.log(`Getting property: ${String(property)}`);
    // 实现属性访问日志
    return Reflect.get(...arguments);
  }
};
const target = { message: "hello" };
const proxy = new Proxy(target, handler);
console.log(proxy.message); 
// Getting property: message
//hello

9.2.2 set()

拦截的操作:属性设置、对应的反射API:Reflect.set()

返回值:返回true表示成功,false表示失败,严格模式下抛出TypeError
触发场景

  • proxy.property = value
  • proxy[property] = value
  • Object.create(proxy)[property] = value
  • Reflect.set(proxy, property, value, receiver)

参数:

  • target: 目标对象。
  • property: 引用的目标对象上的键属性
  • value: 要赋给属性的值
  • receiver: 接收最初赋值的对象
const handler = {
  set(target, property, value, receiver) {
    console.log(`Setting ${String(property)} to ${value}`);

    // 数据验证示例
    if (property === 'age' && (typeof value !== 'number' || value < 0)) {
      throw new TypeError('Age must be a positive number');
    }

    return Reflect.set(...arguments);
  }
};

const proxy = new Proxy({}, handler);
proxy.name = "Alice"; // Setting name to Alice
proxy.age = 25;       // Setting age to 25
// proxy.age = -5;    // 抛出 TypeError

9.2.3 has()

拦截的操作in 操作符、对应的反射API:Reflect.has()

返回值:必须返回布尔值,表示是否存在属性。
触发场景

  • property in proxy
  • property in Object.create(proxy)
  • with(proxy) { (property); }
  • Reflect.has(proxy, property)

参数:

  • target: 目标对象。
  • property: 引用的目标对象上的键属性
const handler = {
  has(target, property) {
    // 隐藏以_开头的"私有"属性
    if (typeof property === 'string' && property.startsWith('_')) {
      return false;
    }
    return Reflect.has(...arguments);
  }
};

const target = { 
  publicData: "accessible", 
  _secret: "hidden" 
};
const proxy = new Proxy(target, handler);

console.log('publicData' in proxy); // true
console.log('_secret' in proxy);    // false
console.log('_secret' in target);   // true

9.2.4 deleteProperty()

拦截的操作delete 操作符、对应的反射API:Reflect.deleteProperty()

返回值:必须返回布尔值,表示是否删除属性成功。
触发场景

  • delete proxy.property
  • delete proxy[property]
  • Reflect.deleteProperty(proxy, property)

参数:

  • target: 目标对象。
  • property: 引用的目标对象上的键属性
const handler = {
  deleteProperty(target, property) {
    // 防止删除重要属性
    if (property === 'id') {
      throw new Error('Cannot delete id property');
    }
    console.log(`Deleting property: ${property}`);
    return Reflect.deleteProperty(...arguments);
  }
};

const target = { id: 1, temp: "can be deleted" };
const proxy = new Proxy(target, handler);

delete proxy.temp; // Deleting property: temp
// delete proxy.id; // 抛出 Error

9.2.5 apply()

拦截的操作:函数调用、call()apply()、对应的反射API:Reflect.apply()

返回值:无限制。
触发场景

  • proxy(...argumentsList)
  • Function.prototype.apply(thisArgument, argumentsList)
  • Function.prototype.call(thisArgument, ...argumentsList)
  • Reflect.apply(target, thisArgument, argumentsList)

参数:

  • target: 目标对象
  • thisArg: 调用函数时的this参数
  • argumentsList: 调用函数时的参数列表
const handler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Called with: ${argumentsList.join(', ')}`);

    // 性能监控
    const start = performance.now();
    const result = Reflect.apply(...arguments);
    const end = performance.now();

    console.log(`Execution time: ${end - start}ms`);
    return result;
  }
};

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

const proxy = new Proxy(sum, handler);
console.log(proxy(5, 3));
// Called with: 5, 3
// Execution time: 0.1ms
// 8

9.2.6 construct()

拦截的操作new 操作符、对应的反射API:Reflect.construct()

返回值:必须返回一个对象。
触发场景

  • new proxy(...args)
  • Reflect.construct(target, argumentsList, newTarget)

参数:

  • target: 目标构造函数
  • argumentsList: 传给目标构造函数的参数列表
  • newTarget: 最初被调用的构造函数
const handler = {
  construct(target, argumentsList, newTarget) {
    console.log(`Constructing with args: ${argumentsList}`);

    // 单例模式实现
    if (!target.instance) {
      target.instance = Reflect.construct(...arguments);
    }
    return target.instance;
  }
};

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

const ProxyUser = new Proxy(User, handler);
const user1 = new ProxyUser("Alice");
const user2 = new ProxyUser("Bob");

console.log(user1 === user2); // true - 单例
console.log(user1.name);      // Alice
console.log(user2.name);      // Alice (不是 Bob!)

9.2.7 getPrototypeOf()

拦截的操作Object.getPrototypeOf()、对应的反射API:Reflect.getPrototypeOf()

返回值:必须返回一个对象或null。
触发场景

  • Object.getPrototypeOf(proxy)
  • Reflect.getPrototypeOf(proxy)
  • proxy.__proto__
  • Object.prototype.isPrototypeOf(proxy)
  • instanceof

参数:

  • target: 目标对象。
const handler = {
  getPrototypeOf(target) {
    console.log('getPrototypeOf called');
    // 可以返回一个假的原型
    return Array.prototype; // 让普通对象伪装成数组
  }
};

const target = {};
const proxy = new Proxy(target, handler);

console.log(Object.getPrototypeOf(proxy));
// getPrototypeOf called
//	Object(0) []

9.2.8 setPrototypeOf()

拦截的操作Object.setPrototypeOf()、对应的反射API:Reflect.setPrototypeOf()

返回值:必须返回布尔值,表示原型赋值是否成功。
触发场景

  • Object.setPrototypeOf(proxy)
  • Reflect.setPrototypeOf(proxy)

参数:

  • target: 目标对象。
  • prototype: target的替代原型,如果是顶级原型则为null.
const handler = {
  setPrototypeOf(target, prototype) {
    console.log(`Setting prototype to: ${prototype}`);

    // 阻止修改原型
    if (prototype !== Object.prototype) {
      throw new Error('Prototype cannot be changed');
    }
    return Reflect.setPrototypeOf(...arguments);
  }
};

const target = {};
const proxy = new Proxy(target, handler);

Object.setPrototypeOf(proxy, Object.prototype); // 允许
// Object.setPrototypeOf(proxy, Array.prototype); // 抛出 Error

9.2.9 isExtensible()

拦截的操作Object.isExtensible()、对应的反射API:Reflect.isExtensible()

返回值:必须返回布尔值,表示target是否可扩展。
触发场景

  • Object.isExtensible(proxy)
  • Reflect.isExtensible(proxy)

参数:

  • target: 目标对象。
const handler = {
  isExtensible(target) {
    console.log('isExtensible called');
    // 可以返回固定值,隐藏真实的可扩展性
    return false; // 总是返回不可扩展
  }
};

const target = {};
const proxy = new Proxy(target, handler);

console.log(Object.isExtensible(proxy)); 
// isExtensible called
// false
console.log(Object.isExtensible(target)); // true

9.2.10 preventExtensions()

拦截的操作Object.preventExtensions()、对应的反射API:Reflect.preventExtensions()

返回值:必须返回布尔值,表示target是否已经不可扩展。
触发场景

  • Object.preventExtensions(proxy)
  • Reflect.preventExtensions(proxy)

参数:

  • target: 目标对象。
const handler = {
  preventExtensions(target) {
    console.log('preventExtensions called');
    // 可以阻止真正的不可扩展操作
    return false; // 阻止操作
  }
};

const target = {};
console.log(Object.isExtensible(target)); // true - 目标仍然可扩展
const proxy = new Proxy(target, handler);
const result = Object.preventExtensions(proxy);
console.log(result); // false - 操作失败
//true
//preventExtensions called
//TypeError: 'preventExtensions' on proxy: trap returned falsish

9.2.11 getOwnPropertyDescriptor()

拦截的操作Object.getOwnPropertyDescriptor()、对应的反射API:Reflect.getOwnPropertyDescriptor()

返回值:必须返回对象,或者在属性不存在时返回undefined。
触发场景

  • Object.getOwnPropertyDescriptor(proxy, property)
  • Reflect.getOwnPropertyDescriptor(proxy, property)

参数:

  • target: 目标对象。
  • property: 引用的目标对象上的键属性
const handler = {
  getOwnPropertyDescriptor(target, property) {
    console.log(`getOwnPropertyDescriptor for: ${property}`);

    // 可以为不存在的属性返回描述符
    if (property === 'virtualProperty') {
      return {
        value: 'I am virtual',
        writable: true,
        enumerable: true,
        configurable: true
      };
    }
    return Reflect.getOwnPropertyDescriptor(...arguments);
  }
};

const target = { realProperty: 'I am real' };
const proxy = new Proxy(target, handler);

console.log(Object.getOwnPropertyDescriptor(proxy, 'realProperty'));
console.log(Object.getOwnPropertyDescriptor(proxy, 'virtualProperty'));
/*
getOwnPropertyDescriptor for: realProperty
{
  value: 'I am real',
  writable: true,
  enumerable: true,
  configurable: true
}
getOwnPropertyDescriptor for: virtualProperty
{
  value: 'I am virtual',
  writable: true,
  enumerable: true,
  configurable: true
}
*/

9.2.12 defineProperty()

拦截的操作Object.defineProperty()、对应的反射API:Reflect.defineProperty()

返回值:必须返回布尔值,表示属性是否定义成功。
触发场景

  • Object.defineProperty(proxy, property, descriptor)
  • Reflect.defineProperty(proxy, property, descriptor)

参数:

  • target:目标对象
  • property: 引用的目标对象上的键属性
  • descriptor: 包含可选的enumerable、configurable、writable、value、get和set定义的对象。
const handler = {
  defineProperty(target, property, descriptor) {
    console.log(`Defining property: ${property}`);

    // 验证属性描述符
    if (property === 'readOnly' && descriptor.writable) {
      throw new Error('readOnly property must be non-writable');
    }

    return Reflect.defineProperty(...arguments);
  }
};

const target = {};
const proxy = new Proxy(target, handler);

Object.defineProperty(proxy, 'normal', { value: 42, writable: true });
//Defining property: normal
// 下面这行会抛出错误:
// Object.defineProperty(proxy, 'readOnly', { value: 42, writable: true });

9.2.13 ownKeys()

拦截的操作Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Reflect.ownKeys()

返回值:必须返回包含字符串或符号的可枚举对象。
触发场景

  • Object.keys(proxy)
  • Object.getOwnPropertyNames(proxy)
  • Object.getOwnPropertySymbols(proxy)
  • Reflect.ownKeys(proxy)

参数:

  • target:目标对象
const handler = {
  ownKeys(target) {
    console.log('ownKeys called');

    // 过滤掉私有属性(以_开头)
    const actualKeys = Reflect.ownKeys(target);
    return actualKeys.filter(key => {
      const keyStr = String(key);
      return !keyStr.startsWith('_');
    });
  }
};

const target = {
  public1: 'value1',
  _private: 'secret',
  public2: 'value2',
  [Symbol('privateSymbol')]: 'hidden'
};

const proxy = new Proxy(target, handler);

console.log(Object.keys(proxy));       
// ownKeys called
// ['public1', 'public2']
console.log(Object.getOwnPropertyNames(proxy)); 
// ownKeys called
// ['public1', 'public2']

9.3 代理模式

使用代理可以在代码中实现一些有用的编程:

  1. 跟踪属性访问:通过捕获get、set和has等操作,可以知道对象属性何时被访问。
  2. 隐藏属性:代理内部实现对外部代码是不可见的,因此可以通过捕获get和has操作因此目标对象上的部分属性。
  3. 属性验证:因为所有赋值操作都会触发set()捕获器,因此可以通过添加校验逻辑决定是否允许赋值操作
  4. 函数与构造函数参数验证:可以对传递的参数进行审查,如限定传参类型。
  5. 数据绑定与可观察对象:把运行时中原本不想关的部分联系到一起,实现各种模式,让不同的代码互操作。例如将被代理的类绑定到一个全局实例集合,让所有创建的实力都被添加进去;也可以将集合绑定到一个事件分派程序,每次插入新实例都会发送消息。

9.4 现代应用场景与最佳实践

1. Vue 3 的响应式系统

Vue 2 使用 Object.defineProperty 来实现数据响应式,它有诸多限制(如无法检测属性的添加/删除,对数组索引修改不敏感等)。

Vue 3 全面转向了 Proxy

  • 原理: Vue 3 的 reactive() 函数返回一个原始对象的代理。
    • get 陷阱用于跟踪依赖(Track):当组件读取属性时,记录下这个属性与当前副作用(例如,组件的渲染函数)的关系。
    • set 陷阱用于触发更新(Trigger):当属性被修改时,通知所有依赖于这个属性的副作用重新执行。
  • 优势
    • 直接监控数组索引和 length 变化。
    • 直接监控对象的属性添加和删除。
    • 性能更好,实现了惰性递归代理(只有被访问到的嵌套对象才会被代理)。
// 一个极简的Vue 3响应式原理示意
function reactive(target) {
  return new Proxy(target, {
    get(obj, key) {
      track(obj, key); // 依赖收集
      return Reflect.get(obj, key);
    },
    set(obj, key, value) {
      const oldValue = obj[key];
      const result = Reflect.set(obj, key, value);
      if (oldValue !== value) { // 只有值真正改变时才触发
        trigger(obj, key); // 触发更新
      }
      return result;
    }
  });
}
2. 更强大的库和框架

许多现代库都深度使用 Proxy:

  • Immer: 用于创建不可变数据状态。它通过 Proxy 拦截所有对“草稿”(draft)状态的修改,并在最后生成一个新的不可变状态。这让你可以用可变(mutable)的语法,写出不可变(immutable)的逻辑。
  • MobX: 状态管理库,其核心原理与 Vue 3 类似,使用 Proxy/Reflect 进行依赖追踪和更新触发。
3. 浏览器和Node.js的增强支持

随着ES6+的全面普及,Proxy在所有现代浏览器和Node.js中都得到了稳定支持。开发者可以放心地在生产环境中使用,无需polyfill。

4.最佳实践
  • 始终使用Reflect: 在代理陷阱中,除非你有明确理由不执行默认行为,否则总是调用对应的 Reflect 方法。这能确保代理对象的行为与普通对象保持一致。
  • 保持透明性: 设计代理时,尽量让它的行为对使用者透明,避免引入令人困惑的“魔法”。
  • 渐进采用: 在大型项目中,先在非核心、可控制的部分引入代理,充分测试后再逐步推广。
Logo

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

更多推荐