一、什么是JavaScript代理?

JavaScript中的Proxy是ES6引入的一个强大特性,它允许你创建一个对象的代理,从而拦截并自定义该对象的基本操作,如属性查找、赋值、枚举、函数调用等。Proxy为JavaScript提供了元编程能力,让开发者能够在运行时动态地拦截和修改对象行为。

 Proxy的基本语法

const proxy = new Proxy(target, handler);
  • target:要代理的目标对象(可以是任何类型的对象,包括数组、函数,甚至是另一个代理)。

  • handler:一个对象,包含各种拦截器方法,这些方法会在执行相应操作时被调用。

  • proxy:生成的代理对象,对代理对象的操作会被转发到目标对象,并通过handler中的拦截器方法进行处理。

Proxy的拦截器方法

Proxy提供了13种拦截器方法,也称为陷阱(traps),它们对应JavaScript中对象的各种基本操作:

属性访问相关

方法

描述

handler.get(target, property, receiver)

拦截属性查找操作

handler.set(target, property, value, receiver)

拦截属性设置操作

handler.deleteProperty(target, property)

拦截属性删除操作

handler.has(target, property)

拦截in操作符

handler.getOwnPropertyDescriptor(target, property)

拦截Object.getOwnPropertyDescriptor()

handler.defineProperty(target, property, descriptor)

拦截Object.defineProperty()

原型相关

方法

描述

handler.getPrototypeOf(target)

拦截Object.getPrototypeOf()

handler.setPrototypeOf(target, prototype)

拦截Object.setPrototypeOf()

枚举相关

方法

描述

handler.ownKeys(target)

拦截Object.keys()、Object.getOwnPropertyNames()等

handler.preventExtensions(target)

拦截Object.preventExtensions()

handler.isExtensible(target)

拦截Object.isExtensible()

函数调用相关

方法

描述

handler.apply(target, thisArg, argumentsList)

拦截函数调用操作

handler.construct(target, argumentsList, newTarget)

拦截new操作符

二、Proxy的核心特性与机制

1. 透明代理

当handler不包含任何拦截器时,代理会完全透明地转发所有操作到目标对象:

const target = { name: '目标对象' };
const handler = {};
const proxy = new Proxy(target, handler);

console.log(proxy.name); // 输出: 目标对象
proxy.name = '新名称';
console.log(target.name); // 输出: 新名称
2. 上下文保留(receiver参数)

receiver参数确保了正确的上下文绑定,特别是在涉及继承和setter/getter时:

const parent = {
  get name() {
    return this._name || '默认名称';
  },
  set name(value) {
    this._name = value;
  }
};

const handler = {
  get(target, property, receiver) {
    console.log(`访问属性: ${property}`);
    // 使用Reflect.get确保this绑定到receiver(即代理对象)
    return Reflect.get(target, property, receiver);
  }
};

const proxy = new Proxy(parent, handler);
const child = Object.create(proxy);
child._name = '子对象';

console.log(child.name); // 输出: 访问属性: name 和 子对象
// 如果不使用receiver,这里会返回'默认名称'
3. 不可撤销性

一旦创建了Proxy,就无法撤销它,这意味着代理和目标对象之间的连接将一直存在:

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

// 无法撤销这个代理
4. 代理对象的特殊性质
  • 代理对象没有自己的属性(除非在拦截器中显式添加)。

  • typeof proxy 会根据目标对象的类型返回相应的结果。

  • instanceof 操作符会正确地穿过代理对象。

 三、Proxy的高级应用示例

1. 数据验证与保护示例
function createValidator(target, validations) {
  return new Proxy(target, {
    set(obj, prop, value) {
      // 检查是否有该属性的验证规则
      if (validations.hasOwnProperty(prop)) {
        const isValid = validations[prop](value);
        if (!isValid) {
          throw new Error(`属性 ${prop} 的值 ${value} 无效`);
        }
      }
      // 通过验证后设置属性
      return Reflect.set(obj, prop, value);
    },
    get(obj, prop) {
      // 保护私有属性
      if (typeof prop === 'string' && prop.startsWith('_')) {
        throw new Error(`无法访问私有属性: ${prop}`);
      }
      return Reflect.get(obj, prop);
    },
    deleteProperty(obj, prop) {
      // 防止删除某些关键属性
      const protectedProps = ['id', 'createdAt'];
      if (protectedProps.includes(prop)) {
        throw new Error(`属性 ${prop} 无法删除`);
      }
      return Reflect.deleteProperty(obj, prop);
    }
  });
}

// 使用示例
const userSchema = {
  name: value => typeof value === 'string' && value.length >= 2,
  age: value => typeof value === 'number' && value >= 0 && value <= 150,
  email: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};

const user = createValidator({}, userSchema);
user.name = '文曲'; // 有效
user.age = 30; // 有效
// user.age = 200; // 无效,会抛出错误
// user._password = 'secret'; // 尝试访问私有属性会抛出错误
2. 响应式数据系统示例
function reactive(target, callback) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const oldValue = obj[prop];
      const result = Reflect.set(obj, prop, value);
      // 只有当值发生变化时才触发回调
      if (oldValue !== value) {
        callback(prop, oldValue, value);
      }
      return result;
    }
  });
}

// 使用示例 - 创建一个简单的响应式计数器
const counter = reactive({ count: 0 }, (prop, oldValue, newValue) => {
  console.log(`属性 ${prop} 从 ${oldValue} 变为 ${newValue}`);
  // 这里可以更新UI等操作
  updateCounterDisplay(newValue);
});

function updateCounterDisplay(value) {
  console.log(`UI更新: 当前计数为 ${value}`);
}

// 使用计数器
counter.count++; // 触发回调
counter.count = 10; // 触发回调
counter.count = 10; // 不会触发回调,因为值没有变化
3. 虚拟属性和计算属性示例
function withComputed(target, computedProps) {
  return new Proxy(target, {
    get(obj, prop) {
      // 检查是否是计算属性
      if (computedProps.hasOwnProperty(prop)) {
        const computeFn = computedProps[prop];
        return computeFn(obj);
      }
      return Reflect.get(obj, prop);
    },
    // 防止直接修改计算属性
    set(obj, prop, value) {
      if (computedProps.hasOwnProperty(prop)) {
        throw new Error(`无法直接设置计算属性: ${prop}`);
      }
      return Reflect.set(obj, prop, value);
    }
  });
}

// 使用示例
const person = withComputed(
  { firstName: '文', lastName: '曲', age: 30 },
  {
    fullName: obj => `${obj.firstName}${obj.lastName}`,
    birthYear: obj => new Date().getFullYear() - obj.age,
    isAdult: obj => obj.age >= 18
  }
);

console.log(person.fullName); // 文曲
console.log(person.birthYear); // 根据当前年份和年龄计算
console.log(person.isAdult); // true
person.firstName = '武';
console.log(person.fullName); // 武曲 (动态更新)
// person.fullName = '破军'; // 会抛出错误,无法直接设置计算属性
4. 方法调用追踪与日志
function traceMethodCalls(target) {
    return new Proxy(target, {
        get(obj, prop) {
            const value = Reflect.get(obj, prop);
            // 只代理方法
            if (typeof value === 'function') {
                return function (...args) {
                    console.log(`调用方法 ${prop},参数: ${args}`);
                    const startTime = performance.now();
                    try {
                        const result = value.apply(this, args);
                        console.log(`方法 ${prop} 返回:${result}`);
                        return result;
                    } catch(error) {
                        console.error(`方法 ${prop} 抛出错误:`, error);
                        throw error;
                    } finally {
                        const endTime = performance.now();
                        console.log(`方法 ${prop} 执行时间: ${endTime - startTime} ms`);
                    }
                }
            }
            return value;
        }
    });
}

// 示例:追踪 Math 对象的方法调用
const tracedMath = traceMethodCalls(Math);
console.log(tracedMath.random()); // 调用 Math.random() 方法

// 示例:追踪简单计算器对象的方法调用
const calculator = traceMethodCalls({
    add(a, b) {
        return a + b;
    },
    subtract(a, b) {
        return a - b;
    },
    multiply(a, b) {
        return a * b;
    },
    divide(a, b) {
        if (b === 0) {
            throw new Error('除数不能为零');
        }
        return a / b;
    }
});
console.log(calculator.add(5, 3)); // 调用 add 方法
console.log(calculator.divide(10, 2)); // 调用 divide 方法
console.log(calculator.multiply(4, 5)); // 调用 multiply 方法
console.log(calculator.subtract(10, -4)); // 调用 subtract 方法
// console.log(calculator.divide(8, 0)); // 会抛出错误

四、浏览器兼容性

Proxy在现代浏览器中得到了广泛支持,但在IE11及更早版本中不可用:

点击 《Proxy 浏览器兼容性》 ,查看更多的兼容信息。

五、Proxy 的主要应用场景

1. 数据绑定与响应式系统
  • 双向数据绑定:通过拦截属性的 get/set 操作,实现数据变化时自动更新视图。

  • 依赖追踪:记录哪些函数依赖于哪些属性,实现精确的更新(如 Vue 3 的响应式系统核心)。

2. 属性访问控制与验证
  • 只读属性:防止对特定属性的修改。

  • 类型检查:在设置属性时验证值的类型是否正确。

  • 权限控制:根据不同用户或条件限制对属性的访问。

3. 虚拟属性与计算属性
  • 动态属性:根据其他属性动态计算值(见:虚拟属性和计算属性示例)。

  • 懒加载属性:只有在首次访问时才计算或加载值。

  • 属性转换:在获取或设置属性时自动转换数据格式。

4. 数据代理与封装
  • 远程代理:本地对象代理远程数据访问。

  • 缓存代理:为开销大的操作结果提供缓存。

  • 访问日志:记录对对象的所有访问操作。

5. 对象属性扩展
  • 默认值处理:当访问不存在的属性时返回默认值。

  • 属性规范化:自动处理大小写、连字符等格式转换。

  • 防抖/节流:为频繁的属性设置操作添加防抖或节流功能。

 6. 构建框架和工具
  • ORM 实现:映射数据库操作到对象方法。

  • 状态管理:管理应用状态,提供可预测的状态更新。

  • API 适配器:统一不同 API 的接口形式。

7. 拦截器和中间件
  • 错误处理:统一捕获对象操作中的错误。

  • 性能监控:测量属性访问或方法调用的性能。

  • AOP(面向切面编程):在不修改原始代码的情况下增强功能。

 8. 安全控制
  • 沙箱隔离:限制对象的访问范围。

  • 输入验证:过滤不安全的输入值。

  • 访问审计:记录敏感属性的访问历史。

六、Proxy的局限性

  • 某些内置对象的方法无法被拦截:如Date对象的某些方法。

  • 不能代理不可配置或不可写的属性:除非在handler的set中返回true。

  • 无法直接拦截某些操作:如访问对象的内部插槽(internal slots)。

  • 与WeakMap/WeakSet一起使用时有限制:无法作为这些集合的键进行跟踪。

总结

JavaScript的Proxy是一个强大的元编程工具,它允许开发者在运行时拦截和自定义对象的基本操作。通过Proxy,我们可以实现数据验证、响应式系统、计算属性、方法追踪等高级功能。合理使用Proxy,可以让代码更加灵活、可维护,并能构建出更加强大和智能的应用程序架构。

Proxy与Reflect、Symbol等其他ES6特性结合使用,可以发挥出更强大的元编程能力,但在使用时也需要注意性能和兼容性问题。

彩蛋

完整示例代码,请移步Cnb 《追踪方法调用示例