joken-前端工程师

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: :: :: 管理 ::

proxy 有哪些拦截器

JavaScript Proxy 拦截器大全

Proxy 提供了13种拦截器(traps),可以拦截目标对象的几乎所有基本操作。以下是完整的拦截器列表及其用途:

1. 基本操作拦截器

get(target, property, receiver)

  • 拦截属性读取:proxy.fooproxy['foo']
  • 参数:目标对象、属性名、Proxy 或继承对象

set(target, property, value, receiver)

  • 拦截属性设置:proxy.foo = barproxy['foo'] = bar
  • 参数:目标对象、属性名、属性值、Proxy 或继承对象
  • 需要返回布尔值表示是否设置成功

has(target, property)

  • 拦截 in 操作符:'foo' in proxy
  • 参数:目标对象、属性名
  • 需要返回布尔值

2. 函数调用拦截器

apply(target, thisArg, argumentsList)

  • 拦截函数调用:proxy(...args)
  • 参数:目标函数、this 值、参数列表

construct(target, argumentsList, newTarget)

  • 拦截 new 操作:new proxy(...args)
  • 参数:目标类、参数列表、最初被调用的构造函数

3. 对象属性管理拦截器

defineProperty(target, property, descriptor)

  • 拦截 Object.defineProperty()
  • 参数:目标对象、属性名、属性描述符
  • 需要返回布尔值表示是否成功

deleteProperty(target, property)

  • 拦截 delete 操作:delete proxy.foo
  • 参数:目标对象、属性名
  • 需要返回布尔值表示是否成功

getOwnPropertyDescriptor(target, property)

  • 拦截 Object.getOwnPropertyDescriptor()
  • 参数:目标对象、属性名

4. 原型相关拦截器

getPrototypeOf(target)

  • 拦截 Object.getPrototypeOf()
  • 参数:目标对象

setPrototypeOf(target, prototype)

  • 拦截 Object.setPrototypeOf()
  • 参数:目标对象、新原型对象
  • 需要返回布尔值表示是否成功

5. 可扩展性相关拦截器

isExtensible(target)

  • 拦截 Object.isExtensible()
  • 参数:目标对象
  • 需要返回布尔值

preventExtensions(target)

  • 拦截 Object.preventExtensions()
  • 参数:目标对象
  • 需要返回布尔值表示是否成功

6. 属性枚举拦截器

ownKeys(target)

  • 拦截:
    • Object.getOwnPropertyNames()
    • Object.getOwnPropertySymbols()
    • Object.keys()
    • for...in 循环
  • 参数:目标对象
  • 需要返回数组

7. 完整拦截器示例

const handler = {
  // 基本操作
  get: function(target, prop, receiver) {
    console.log(`GET ${prop}`);
    return Reflect.get(...arguments);
  },
  
  set: function(target, prop, value, receiver) {
    console.log(`SET ${prop}=${value}`);
    return Reflect.set(...arguments);
  },
  
  has: function(target, prop) {
    console.log(`HAS ${prop}?`);
    return Reflect.has(...arguments);
  },
  
  // 函数调用
  apply: function(target, thisArg, argumentsList) {
    console.log(`CALL with`, argumentsList);
    return Reflect.apply(...arguments);
  },
  
  construct: function(target, argumentsList, newTarget) {
    console.log(`NEW with`, argumentsList);
    return Reflect.construct(...arguments);
  },
  
  // 属性管理
  defineProperty: function(target, prop, descriptor) {
    console.log(`DEFINE ${prop}`, descriptor);
    return Reflect.defineProperty(...arguments);
  },
  
  deleteProperty: function(target, prop) {
    console.log(`DELETE ${prop}`);
    return Reflect.deleteProperty(...arguments);
  },
  
  getOwnPropertyDescriptor: function(target, prop) {
    console.log(`DESCRIBE ${prop}`);
    return Reflect.getOwnPropertyDescriptor(...arguments);
  },
  
  // 原型相关
  getPrototypeOf: function(target) {
    console.log('GET PROTOTYPE');
    return Reflect.getPrototypeOf(...arguments);
  },
  
  setPrototypeOf: function(target, proto) {
    console.log('SET PROTOTYPE', proto);
    return Reflect.setPrototypeOf(...arguments);
  },
  
  // 可扩展性
  isExtensible: function(target) {
    console.log('IS EXTENSIBLE?');
    return Reflect.isExtensible(...arguments);
  },
  
  preventExtensions: function(target) {
    console.log('PREVENT EXTENSIONS');
    return Reflect.preventExtensions(...arguments);
  },
  
  // 属性枚举
  ownKeys: function(target) {
    console.log('GET OWN KEYS');
    return Reflect.ownKeys(...arguments);
  }
};

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

拦截器使用场景总结

拦截器 典型应用场景
get 访问控制、惰性加载、数据转换
set 数据验证、自动持久化、变更通知
has 隐藏属性、权限控制
apply 函数调用包装、日志记录
construct 单例模式、实例验证
defineProperty 属性定义限制
deleteProperty 防止重要属性被删除
getPrototypeOf 隐藏真实原型
setPrototypeOf 阻止原型修改
ownKeys 过滤属性枚举

注意事项

  1. 不是所有拦截器都必须实现,只需实现需要的部分
  2. 在拦截器中通常应该使用 Reflect 对应的方法来完成默认行为
  3. 某些拦截器需要返回布尔值表示操作是否成功
  4. 过度使用 Proxy 可能会影响性能
  5. 某些操作无法被拦截,如 typeofinstanceof

Proxy 的这些拦截器提供了强大的元编程能力,可以用来实现各种高级模式如数据绑定、不可变对象、观察者模式等。

Reflect

JavaScript Reflect 对象详解

Reflect 是 ES6 引入的一个内置对象,它提供了一系列静态方法,这些方法与 Proxy 的拦截器(traps)一一对应,用于执行对象的基本操作。

Reflect 的主要作用

  1. 提供操作对象的统一 API:将一些原本分散的操作(如 Object.definePropertydelete 等)统一到 Reflect 对象上
  2. 与 Proxy 配合使用:在 Proxy 拦截器中,通常使用 Reflect 方法来执行默认行为
  3. 更合理的返回值:相比一些旧方法,Reflect 方法有更合理的返回值(如返回布尔值表示操作是否成功)

Reflect 的常用方法

1. Reflect.get(target, propertyKey[, receiver])

获取对象属性的值

const obj = { x: 1, y: 2 };
console.log(Reflect.get(obj, 'x')); // 1

2. Reflect.set(target, propertyKey, value[, receiver])

设置对象属性的值

const obj = {};
Reflect.set(obj, 'name', 'Alice');
console.log(obj.name); // "Alice"

3. Reflect.has(target, propertyKey)

检查对象是否包含某属性(相当于 in 操作符)

const obj = { name: 'Bob' };
console.log(Reflect.has(obj, 'name')); // true
console.log(Reflect.has(obj, 'age')); // false

4. Reflect.deleteProperty(target, propertyKey)

删除对象属性(相当于 delete 操作符)

const obj = { x: 1, y: 2 };
Reflect.deleteProperty(obj, 'x');
console.log(obj); // { y: 2 }

5. Reflect.construct(target, argumentsList[, newTarget])

相当于 new 操作符

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

const p = Reflect.construct(Person, ['Alice']);
console.log(p.name); // "Alice"

6. Reflect.apply(func, thisArg, args)

调用函数(类似于 Function.prototype.apply

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

console.log(Reflect.apply(sum, null, [1, 2])); // 3

7. Reflect.defineProperty(target, propertyKey, attributes)

定义对象属性(类似于 Object.defineProperty

const obj = {};
Reflect.defineProperty(obj, 'x', { value: 1, writable: false });
console.log(obj.x); // 1

8. Reflect.getOwnPropertyDescriptor(target, propertyKey)

获取属性描述符(类似于 Object.getOwnPropertyDescriptor

const obj = { x: 1 };
const desc = Reflect.getOwnPropertyDescriptor(obj, 'x');
console.log(desc.value); // 1

9. Reflect.getPrototypeOf(target)

获取对象原型(类似于 Object.getPrototypeOf

const obj = {};
console.log(Reflect.getPrototypeOf(obj) === Object.prototype); // true

10. Reflect.setPrototypeOf(target, prototype)

设置对象原型(类似于 Object.setPrototypeOf

const obj = {};
const proto = { method() {} };
Reflect.setPrototypeOf(obj, proto);
console.log(obj.method); // function method() {}

Reflect 与 Proxy 的关系

Reflect 方法与 Proxy 的拦截器一一对应,通常在 Proxy 拦截器中使用 Reflect 方法来执行默认行为:

const proxy = new Proxy({}, {
  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);
  }
});

为什么使用 Reflect

  1. 更合理的返回值:许多 Reflect 方法返回布尔值表示操作是否成功,比旧方法更合理

    • 例如:Reflect.defineProperty 返回布尔值,而 Object.defineProperty 返回对象或抛出错误
  2. 函数式编程:所有方法都是函数形式,更适合函数式编程风格

  3. 与 Proxy 完美配合:在 Proxy 拦截器中,使用 Reflect 可以方便地执行默认行为

  4. 统一 API:将原本分散在不同地方的 API 统一到 Reflect 对象上

示例:使用 Reflect 实现简单的数据绑定

const data = { name: 'Alice', age: 25 };
const listeners = new Set();

const proxy = new Proxy(data, {
  set(target, prop, value, receiver) {
    const success = Reflect.set(target, prop, value, receiver);
    if (success) {
      listeners.forEach(listener => listener());
    }
    return success;
  }
});

function addListener(listener) {
  listeners.add(listener);
}

addListener(() => console.log('Data changed!'));
proxy.name = 'Bob'; // 输出: "Data changed!"

Reflect 是现代 JavaScript 元编程的重要组成部分,与 Proxy 配合使用可以实现许多高级功能。

如何只拦截劫持自定义的对象属性,而不是对象自带的属性

在 Proxy get 拦截器中区分业务属性和内置属性

要在 Proxy 的 get 拦截器中只处理业务属性而忽略对象的内置属性(如原型方法、Symbol 属性等),可以采用以下几种策略:

方法一:使用属性前缀区分

const handler = {
  get(target, property, receiver) {
    // 只处理以特定前缀开头的属性(如'$')
    if (typeof property === 'string' && property.startsWith('$')) {
      console.log(`访问业务属性: ${property}`);
      return Reflect.get(target, property, receiver);
    }
    
    // 其他属性直接返回
    return Reflect.get(target, property, receiver);
  }
};

const obj = { $name: '业务数据', age: 30, toString: '测试' };
const proxy = new Proxy(obj, handler);

console.log(proxy.$name); // 会触发拦截
console.log(proxy.age);   // 不会触发拦截
console.log(proxy.toString); // 不会触发拦截

方法二:使用白名单机制

const businessProps = new Set(['name', 'age', 'email']);

const handler = {
  get(target, property, receiver) {
    // 只处理白名单中的属性
    if (businessProps.has(property)) {
      console.log(`访问业务属性: ${property}`);
      return Reflect.get(target, property, receiver);
    }
    
    return Reflect.get(target, property, receiver);
  }
};

const obj = { name: 'Alice', age: 25, toString: '测试' };
const proxy = new Proxy(obj, handler);

console.log(proxy.name);     // 会触发拦截
console.log(proxy.age);      // 会触发拦截
console.log(proxy.toString); // 不会触发拦截

方法三:排除内置属性和方法

const handler = {
  get(target, property, receiver) {
    // 排除Symbol属性、原型方法和内置属性
    if (
      typeof property !== 'symbol' && 
      !Object.prototype.hasOwnProperty.call(Object.prototype, property) &&
      !(property in Function.prototype)
    ) {
      console.log(`访问业务属性: ${property}`);
      return Reflect.get(target, property, receiver);
    }
    
    return Reflect.get(target, property, receiver);
  }
};

const obj = { 
  name: '业务数据', 
  [Symbol('test')]: 'Symbol值',
  toString: '覆盖的toString' 
};
const proxy = new Proxy(obj, handler);

console.log(proxy.name);     // 会触发拦截
console.log(proxy.toString); // 不会触发拦截(虽然被覆盖但仍被视为内置属性)
console.log(proxy[Object.getOwnPropertySymbols(obj)[0]]); // 不会触发拦截

方法四:使用属性描述符判断

const handler = {
  get(target, property, receiver) {
    const desc = Object.getOwnPropertyDescriptor(target, property);
    
    // 只处理可枚举的自有属性
    if (desc && desc.enumerable) {
      console.log(`访问业务属性: ${property}`);
      return Reflect.get(target, property, receiver);
    }
    
    return Reflect.get(target, property, receiver);
  }
};

const obj = Object.create(null, {
  name: { value: 'Alice', enumerable: true },
  age: { value: 25, enumerable: false },
  [Symbol('test')]: { value: 'Symbol', enumerable: true }
});

const proxy = new Proxy(obj, handler);

console.log(proxy.name); // 会触发拦截
console.log(proxy.age);  // 不会触发拦截
console.log(proxy[Object.getOwnPropertySymbols(obj)[0]]); // 会触发拦截

最佳实践建议

  1. 明确业务属性定义:最好预先定义哪些属性是业务属性(如使用前缀、白名单等)
  2. 考虑性能:复杂的判断逻辑会影响性能,在频繁访问的场景要谨慎
  3. 处理继承属性:如果需要处理继承来的业务属性,需要额外判断
  4. Symbol属性处理:明确是否需要拦截Symbol属性

完整示例:结合多种策略

const BUSINESS_PREFIX = '$';
const builtInProps = new Set(
  Object.getOwnPropertyNames(Object.prototype)
    .concat(Object.getOwnPropertyNames(Function.prototype))
);

const handler = {
  get(target, property, receiver) {
    // 1. 排除Symbol属性
    if (typeof property === 'symbol') {
      return Reflect.get(target, property, receiver);
    }
    
    // 2. 排除内置属性
    if (builtInProps.has(property)) {
      return Reflect.get(target, property, receiver);
    }
    
    // 3. 只处理特定前缀的业务属性
    if (property.startsWith(BUSINESS_PREFIX)) {
      console.log(`处理业务属性 ${property}`);
      const value = Reflect.get(target, property, receiver);
      // 这里可以添加业务逻辑处理
      return value.toUpperCase(); // 示例:转换为大写
    }
    
    // 其他属性直接返回
    return Reflect.get(target, property, receiver);
  }
};

const obj = {
  $name: 'alice',
  $id: '123',
  age: 30,
  toString: 'test',
  [Symbol('test')]: 'symbol value'
};

const proxy = new Proxy(obj, handler);

console.log(proxy.$name);    // 处理业务属性 $name → ALICE
console.log(proxy.age);      // 30 (不处理)
console.log(proxy.toString); // "test" (不处理)
console.log(proxy[Object.getOwnPropertySymbols(obj)[0]]); // "symbol value" (不处理)

这种方法可以精确控制哪些属性需要被拦截处理,而哪些属性应该保持原样。

posted on 2025-03-27 21:32  joken1310  阅读(131)  评论(0)    收藏  举报