网站Logo 小李的博客

Node.js 中的 Proxy 完全指南

xiaoli
9
2025-05-20

Node.js 中的 Proxy 完全指南

在 JavaScript 的世界里,Proxy 是一个强大而灵活的特性,它允许我们拦截并自定义对象的基本操作。无论是进行数据验证、实现观察者模式、还是创建智能对象,Proxy 都能提供优雅的解决方案。本文将深入探讨 Node.js 中 Proxy 的使用方法、各种 trap 机制、以及使用过程中的注意事项和常见陷阱。

1. Proxy 基础介绍

1.1 什么是 Proxy?

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。简单来说,Proxy 允许你在访问对象时添加额外的逻辑层。

1.2 基本语法

const proxy = new Proxy(target, handler);

其中:

  • target:要代理的目标对象,可以是任何类型的对象,包括原生数组、函数,甚至另一个代理
  • handler:一个包含"陷阱"(traps)的对象,定义了在执行各种操作时代理的行为

1.3 简单示例

让我们从一个基础示例开始,了解 Proxy 的工作方式:

// 目标对象
const target = {
  message: 'Hello, World!'
};

// 处理器对象
const handler = {
  // 拦截属性读取操作
  get(target, property, receiver) {
    console.log(`正在读取 ${property} 属性`);
    return target[property];
  }
};

// 创建代理
const proxy = new Proxy(target, handler);

// 通过代理访问属性
console.log(proxy.message);
// 输出:
// 正在读取 message 属性
// Hello, World!

在这个例子中,我们创建了一个简单的代理,它会在每次读取属性时打印一条消息,然后返回目标对象的属性值。

2. Proxy Trap 方法详解

Proxy 的强大之处在于它提供了多种"陷阱"(traps),用于拦截不同类型的操作。以下是常用的 trap 方法及其详细解释:

2.1 get(target, property, receiver)

拦截对象属性的读取操作。

const handler = {
  get(target, property, receiver) {
    // 提供默认值,避免返回 undefined
    if (!(property in target)) {
      return `属性 ${property} 不存在,返回默认值`;
    }
    return Reflect.get(target, property, receiver);
  }
};

const user = { name: '张三' };
const proxy = new Proxy(user, handler);

console.log(proxy.name); // 张三
console.log(proxy.age); // 属性 age 不存在,返回默认值

2.2 set(target, property, value, receiver)

拦截对象属性的设置操作。

const handler = {
  set(target, property, value, receiver) {
    // 数据验证
    if (property === 'age' && typeof value !== 'number') {
      throw new TypeError('年龄必须是数字');
    }
    if (property === 'age' && value < 0) {
      throw new RangeError('年龄不能为负数');
    }
    
    console.log(`设置 ${property} = ${value}`);
    return Reflect.set(target, property, value, receiver);
  }
};

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

proxy.name = '李四'; // 设置 name = 李四
proxy.age = 25; // 设置 age = 25
// proxy.age = -5; // 抛出 RangeError: 年龄不能为负数
// proxy.age = '三十'; // 抛出 TypeError: 年龄必须是数字

2.3 has(target, property)

拦截 in 操作符。

const handler = {
  has(target, property) {
    console.log(`检查属性 ${property} 是否存在`);
    // 隐藏以下划线开头的私有属性
    if (property.startsWith('_')) {
      return false;
    }
    return property in target;
  }
};

const user = { 
  name: '王五', 
  _password: '123456' 
};
const proxy = new Proxy(user, handler);

console.log('name' in proxy); // 检查属性 name 是否存在 // true
console.log('_password' in proxy); // 检查属性 _password 是否存在 // false

2.4 deleteProperty(target, property)

拦截 delete 操作符。

const handler = {
  deleteProperty(target, property) {
    // 防止删除特定属性
    if (property === 'id') {
      console.log('id 属性不能被删除');
      return false;
    }
    
    console.log(`删除属性 ${property}`);
    return Reflect.deleteProperty(target, property);
  }
};

const user = { id: 1001, name: '赵六' };
const proxy = new Proxy(user, handler);

delete proxy.name; // 删除属性 name
console.log(user); // { id: 1001 }

delete proxy.id; // id 属性不能被删除
console.log(user); // { id: 1001 }

2.5 apply(target, thisArg, argumentsList)

拦截函数调用。

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

const handler = {
  apply(target, thisArg, args) {
    console.log(`函数调用,参数: ${args}`);
    // 可以在调用前后添加逻辑
    const start = Date.now();
    const result = Reflect.apply(target, thisArg, args);
    const end = Date.now();
    console.log(`函数执行耗时: ${end - start}ms`);
    return result;
  }
};

const proxy = new Proxy(sum, handler);
console.log(proxy(10, 20));
// 函数调用,参数: 10,20
// 函数执行耗时: 0ms
// 30

2.6 construct(target, argumentsList, newTarget)

拦截 new 操作符。

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

const handler = {
  construct(target, args, newTarget) {
    console.log(`创建 ${target.name} 实例,参数: ${args}`);
    // 可以修改或增强构造过程
    const instance = Reflect.construct(target, args, newTarget);
    instance.createdAt = new Date();
    return instance;
  }
};

const ProxyUser = new Proxy(User, handler);
const user = new ProxyUser('孙七');
// 创建 User 实例,参数: 孙七
console.log(user); // User { name: '孙七', createdAt: 2025-05-20T... }

2.7 其他常用 trap 方法

除了上述常用的 trap 方法外,Proxy 还提供了以下 trap:

  • getOwnPropertyDescriptor(target, prop):拦截 Object.getOwnPropertyDescriptor()
  • defineProperty(target, property, descriptor):拦截 Object.defineProperty()
  • getPrototypeOf(target):拦截 Object.getPrototypeOf()
  • setPrototypeOf(target, prototype):拦截 Object.setPrototypeOf()
  • isExtensible(target):拦截 Object.isExtensible()
  • preventExtensions(target):拦截 Object.preventExtensions()
  • ownKeys(target):拦截 Object.getOwnPropertyNames()Object.getOwnPropertySymbols()

3. Proxy 的实际应用场景

3.1 数据验证

Proxy 可以用于在设置属性时进行数据验证:

function createValidator(validations) {
  return new Proxy({}, {
    set(target, property, value) {
      if (validations.hasOwnProperty(property)) {
        if (validations[property](value)) {
          target[property] = value;
          return true;
        } else {
          throw new Error(`Invalid value for ${property}`);
        }
      } else {
        target[property] = value;
        return true;
      }
    }
  });
}

const user = createValidator({
  age: value => typeof value === 'number' && value > 0,
  name: value => typeof value === 'string' && value.length > 2
});

user.name = 'Alice'; // 有效
user.age = 25; // 有效
// user.age = -5; // 抛出错误: Invalid value for age

3.2 实现观察者模式

Proxy 可以用于实现观察者模式,当对象属性变化时通知订阅者:

function createObservable(target, callback) {
  return new Proxy(target, {
    set(target, property, value, receiver) {
      const oldValue = target[property];
      const result = Reflect.set(target, property, value, receiver);
      if (oldValue !== value) {
        callback({
          property,
          oldValue,
          newValue: value
        });
      }
      return result;
    }
  });
}

const user = createObservable(
  { name: '张三', age: 30 },
  change => console.log(`属性 ${change.property} 从 ${change.oldValue} 变为 ${change.newValue}`)
);

user.name = '李四'; // 属性 name 从 张三 变为 李四
user.age = 31; // 属性 age 从 30 变为 31

3.3 缓存计算结果

Proxy 可以用于缓存函数调用结果,避免重复计算:

function memoize(fn) {
  const cache = new Map();
  
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);
      if (cache.has(key)) {
        console.log('使用缓存结果');
        return cache.get(key);
      }
      
      const result = Reflect.apply(target, thisArg, args);
      cache.set(key, result);
      console.log('计算新结果');
      return result;
    }
  });
}

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const fastFib = memoize(fibonacci);

console.log(fastFib(40)); // 计算新结果 (可能需要一些时间)
console.log(fastFib(40)); // 使用缓存结果 (立即返回)

4. Proxy 使用注意点和坑点

4.1 基本限制

  1. 不变量(Invariants)强制执行

    JavaScript 强制执行某些不变量,如果 trap 方法违反了这些不变量,会抛出 TypeError。例如,不能修改不可写且不可配置的属性值。

    const target = {};
    Object.defineProperty(target, 'fixed', {
      value: 42,
      writable: false,
      configurable: false
    });
    
    const proxy = new Proxy(target, {
      get(target, prop) {
        if (prop === 'fixed') {
          return 43; // 违反不变量
        }
        return target[prop];
      }
    });
    
    // 抛出 TypeError: 'get' on proxy: property 'fixed' is a read-only and non-configurable...
    // console.log(proxy.fixed);
    
  2. 无法代理基本类型值

    Proxy 只能代理对象和函数,不能代理基本类型如字符串、数字、布尔值等。

  3. 原型链问题

    代理对象与原始对象有不同的引用,这可能导致依赖 instanceofthis 的代码出现问题。

4.2 常见坑点

  1. this 绑定问题

    const target = {
      value: 42,
      getValue() {
        return this.value;
      }
    };
    
    const handler = {};
    const proxy = new Proxy(target, handler);
    
    // 问题:当方法从代理中分离时,this 不再指向代理
    const method = proxy.getValue;
    console.log(method()); // undefined,因为 this 不再绑定到 proxy 或 target
    
    // 解决方案:使用 bind 或箭头函数
    const boundMethod = proxy.getValue.bind(proxy);
    console.log(boundMethod()); // 42
    
  2. 内部插槽访问问题

    某些内置对象(如 Map、Set、Date、Promise 等)依赖于内部插槽,这些插槽不能被代理。

    // 问题示例
    const date = new Date();
    const proxy = new Proxy(date, {});
    // 在某些环境中会抛出 TypeError
    // proxy.getTime(); 
    
    // 解决方案:使用 Reflect 或直接调用原始对象的方法
    console.log(Reflect.apply(Date.prototype.getTime, date, []));
    
  3. 私有字段访问问题

    Proxy 无法拦截对类私有字段的访问。

    class User {
      #password = "secret";
      
      checkPassword(pwd) {
        return pwd === this.#password;
      }
    }
    
    const user = new User();
    const proxy = new Proxy(user, {
      get(target, prop) {
        console.log(`Getting ${prop}`);
        return Reflect.get(target, prop);
      }
    });
    
    // 私有字段访问不会触发 get trap
    proxy.checkPassword("secret"); // 只会记录 "Getting checkPassword"
    
  4. 性能开销

    Proxy 会引入额外的性能开销,特别是在频繁访问属性的代码中。在性能敏感的应用中应谨慎使用。

4.3 解决方案和最佳实践

  1. 使用 Reflect API

    总是配合 Reflect API 使用 Proxy,可以确保正确的行为和错误处理。

    const handler = {
      get(target, prop, receiver) {
        console.log(`Getting ${prop}`);
        return Reflect.get(target, prop, receiver);
      },
      set(target, prop, value, receiver) {
        console.log(`Setting ${prop} to ${value}`);
        return Reflect.set(target, prop, value, receiver);
      }
    };
    
  2. 可撤销代理

    使用 Proxy.revocable() 创建可撤销的代理,在不需要时撤销以避免内存泄漏。

    const target = { message: "hello" };
    const { proxy, revoke } = Proxy.revocable(target, {});
    
    console.log(proxy.message); // "hello"
    
    // 撤销代理
    revoke();
    
    // 抛出 TypeError: Cannot perform 'get' on a proxy that has been revoked
    // console.log(proxy.message);
    
  3. 避免无限递归

    在 trap 方法中调用代理对象自身的方法可能导致无限递归。

    // 错误示例
    const target = {};
    const proxy = new Proxy(target, {
      get(target, prop) {
        // 这会导致无限递归
        // return proxy[prop];
        
        // 正确做法
        return target[prop];
      }
    });
    

5. 进阶应用示例

5.1 实现响应式系统

类似于 Vue.js 的响应式系统,可以使用 Proxy 实现简单的数据绑定:

function reactive(obj) {
  const subscribers = new Map();
  
  function notify(key) {
    if (subscribers.has(key)) {
      subscribers.get(key).forEach(callback => callback());
    }
  }
  
  return new Proxy(obj, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver);
      // 如果是嵌套对象,递归应用 reactive
      if (typeof value === 'object' && value !== null) {
        return reactive(value);
      }
      return value;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        notify(key);
      }
      return result;
    }
  });
}

function watch(obj, key, callback) {
  if (!subscribers.has(key)) {
    subscribers.set(key, new Set());
  }
  subscribers.get(key).add(callback);
}

// 使用示例
const state = reactive({
  count: 0,
  user: {
    name: '张三'
  }
});

watch(state, 'count', () => {
  console.log(`Count changed to ${state.count}`);
});

state.count++; // Count changed to 1

5.2 实现 API 请求缓存

使用 Proxy 可以轻松实现 API 请求的缓存机制:

function createApiClient(baseUrl) {
  const cache = new Map();
  
  const handler = {
    get(target, path) {
      return async (params = {}) => {
        const queryString = new URLSearchParams(params).toString();
        const cacheKey = `${path}?${queryString}`;
        
        if (cache.has(cacheKey)) {
          console.log(`Using cached result for ${cacheKey}`);
          return cache.get(cacheKey);
        }
        
        console.log(`Fetching ${cacheKey}`);
        const url = `${baseUrl}/${path}?${queryString}`;
        const response = await fetch(url);
        const data = await response.json();
        
        cache.set(cacheKey, data);
        return data;
      };
    }
  };
  
  return new Proxy({}, handler);
}

// 使用示例
const api = createApiClient('https://api.example.com');

// 首次请求,会发起网络请求
api.users({ id: 1 }).then(data => console.log(data));

// 重复请求,使用缓存结果
api.users({ id: 1 }).then(data => console.log(data));

6. 总结

Proxy 是 JavaScript 中一个强大的特性,它允许我们拦截并自定义对象的基本操作。通过本文,我们详细了解了:

  1. Proxy 的基本概念和语法
  2. 各种 trap 方法及其用途
  3. Proxy 的实际应用场景
  4. 使用 Proxy 时的注意点和坑点
  5. 进阶应用示例

虽然 Proxy 有一些限制和性能开销,但它提供的元编程能力使得我们能够实现许多强大的功能,如数据验证、观察者模式、响应式系统等。在 Node.js 开发中,合理使用 Proxy 可以让我们的代码更加灵活、强大和优雅。

希望本文能帮助你更好地理解和使用 Proxy,在实际开发中发挥它的强大功能!

动物装饰