详细聊聊Proxy 和 Reflect

在 JavaScript 中,ProxyReflect 是 ECMAScript 6 中新增的两个内置对象,它们提供了一些强大的元编程(meta programming)功能,使得开发人员可以更加灵活地修改和控制对象的行为。本文将分别讲解 ProxyReflect 的用法和场景,最后给出使用 ProxyReflect 结合的示例。

Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等),可以让开发人员拦截和处理这些操作,从而实现元编程功能。

基本语法

Proxy 对象的基本语法如下:

const proxy = new Proxy(target, handler);

其中,target 表示要代理的目标对象,可以是任何对象,handler 是一个对象,其中定义了代理目标对象的行为。handler 可以包含以下一些特殊的属性和方法:

  • get(target, property, receiver):拦截对目标对象属性的读取操作。
  • set(target, property, value, receiver):拦截对目标对象属性的赋值操作。
  • has(target, property):拦截对目标对象是否拥有某个属性的判断操作。
  • deleteProperty(target, property):拦截对目标对象属性的删除操作。
  • apply(target, thisArg, args):拦截对目标对象方法的调用操作。
  • construct(target, args, newTarget):拦截对目标对象的 new 操作。

实际应用

下面是一个示例,展示了如何使用 Proxy 对象来实现一个简单的日志记录器:

const logHandler = {
  get(target, property, receiver) {
    console.log(`Reading property '${property}'...`);
    return Reflect.get(target, property, receiver);
  },
  set(target, property, value, receiver) {
    console.log(`Writing property '${property}' with value '${value}'...`);
    return Reflect.set(target, property, value, receiver);
  },
};

const user = { name: "Alice", age: 30 };
const userProxy = new Proxy(user, logHandler);

userProxy.name; // Reading property 'name'...
userProxy.age = 31; // Writing property 'age' with value '31'...

在上面的代码中,我们定义了一个 logHandler 对象,其中使用了 getset 两个方法来拦截对目标对象属性的读取和赋值操作。具体来说,当程序读取目标对象的属性时,get 方法会输出一个日志信息,然后调用 Reflect.get 方法来实际获取属性值;当程序赋值给目标对象的属性时,set 方法会输出一个日志信息,然后调用 Reflect.set 方法来实际设置属性值。最终,我们将 logHandler 对象作为参数传递给 Proxy构造函数,得到了一个代理对象 userProxy,它可以通过 userProxy 来访问和修改原始的 user 对象。在使用 userProxy.nameuserProxy.age = 31 时,我们可以看到控制台输出了相应的日志信息,从而实现了一个简单的日志记录器。

使用场景

Proxy 对象通常用于以下一些场景:

  • 数据绑定:在 Vue.js 和 React 等框架中,可以使用 Proxy 对象来实现数据绑定功能,即当数据模型发生变化时,可以自动更新视图模型。
  • 权限控制:可以使用 Proxy 对象来实现权限控制功能,即在对某个对象进行操作前,检查当前用户是否有相应的权限,从而避免非法操作。
  • 数据校验:可以使用 Proxy 对象来实现数据校验功能,即在对某个对象进行赋值操作前,对新值进行校验,如果校验不通过,则拒绝赋值。

Reflect

Reflect 对象提供了一组静态方法,可以让开发人员直接调用底层的基础方法,从而实现更加灵活和高效的编程。与 Proxy 对象不同的是,Reflect 对象中的方法与目标对象的方法一一对应,可以直接调用,而不需要使用代理对象。

基本语法

Reflect 对象的基本语法如下:

Reflect.method(target, ...args);

其中,method 表示要调用的方法名,target 表示要操作的目标对象,args 是方法的参数列表。Reflect 对象中包含了大量的方法,包括:

  • Reflect.get(target, property, receiver):获取对象属性的值。
  • Reflect.set(target, property, value, receiver):设置对象属性的值。
  • Reflect.has(target, property):判断对象是否拥有某个属性。
  • Reflect.deleteProperty(target, property):删除对象的属性。
  • Reflect.apply(target, thisArg, args):调用对象的方法。
  • Reflect.construct(target, args, newTarget):创建对象的实例。
  • Reflect.getPrototypeOf(target):获取对象的原型。
  • Reflect.setPrototypeOf(target, proto):设置对象的原型。

实际应用

下面是一个示例,展示了如何使用 Reflect 对象来实现一个简单的事件监听器:

class EventEmitter {
  constructor() {
    this.events = new Map();
  }

  on(event, listener) {
    let listeners = this.events.get(event);
    if (!listeners) {
      listeners = [];
      this.events.set(event, listeners);
    }
    listeners.push(listener);
  }

  emit(event, ...args) {
    const listeners = this.events.get(event);
    if (listeners) {
      for (const listener of listeners) {
        Reflect.apply(listener, this, args);
      }
    }
  }
}

const eventEmitter = new EventEmitter();

eventEmitter.on("hello", (name) => {
  console.log(`Hello, ${name}!`);
});

eventEmitter.on("goodbye", (name) => {
  console.log(`Goodbye, ${name}!`);
});

eventEmitter.emit("hello", "Alice"); // Hello, Alice!
eventEmitter.emit("goodbye", "Bob"); // Goodbye, Bob!

在上面的代码中,我们定义了一个 EventEmitter 类,其中使用了 Reflect.apply 方法来实现事件监听器的功能。具体来说,当程序调用 emit 方法时,我们会首先从 events 映射中获取对应事件的监听器列表,然后使用 Reflect.apply 方法依次调用监听器函数,并将参数传递给它们。最终,我们可以看到控制台输出了相应的信息,从而实现了一个简单的事件监听器。

使用场景

Reflect 对象通常用于以下一些场景:

  • 对象操作:可以使用 Reflect 对象来实现对对象的操作,包括属性读写、方法调用、实例创建等操作,具有更好的性能和可读性。
  • 面向对象编程:可以使用 Reflect 对象来实现面向对象编程的功能,如继承、接口实现等。
  • 代码优化:可以使用 Reflect 对象来优化代码,如减少代码冗余、提高代码可读性等。

Proxy 和 Reflect 的结合使用

ProxyReflect 对象可以结合使用,从而实现更加灵活和高效的编程。

数据绑定

const data = { name: "Alice", age: 30 };
const listeners = new Map();

const dataProxy = new Proxy(data, {
  get(target, property, receiver) {
    const value = Reflect.get(target, property, receiver);
    if (typeof value === "object" && value !== null) {
      return new Proxy(value, dataProxy);
    }
    return value;
  },
  set(target, property, value, receiver) {
    const result = Reflect.set(target, property, value, receiver);
    const propertyListeners = listeners.get(property);
    if (propertyListeners) {
      for (const listener of propertyListeners) {
        Reflect.apply(listener, undefined, [property, value]);
      }
    }
    return result;
  },
});

function onChange(property, listener) {
  let propertyListeners = listeners.get(property);
  if (!propertyListeners) {
    propertyListeners = [];
    listeners.set(property, propertyListeners);
  }
  propertyListeners.push(listener);
}

onChange("name", (property, value) => {
  console.log(`Name changed to ${value}!`);
});

onChange("age", (property, value) => {
  console.log(`Age changed to ${value}!`);
});

dataProxy.name = "Bob"; // Name changed to Bob!
dataProxy.age = 31; // Age changed to 31!

在上面的代码中,我们定义了一个 data 对象,其中包含了两个属性 nameage。然后,我们使用 Proxy 对象 dataProxy 来代理 data 对象,并定义了 getset 两个方法来拦截对 data 对象的读取和赋值操作。具体来说,当程序读取 data 对象的某个属性时,get 方法会返回相应的属性值,并检查该值是否为对象类型,如果是对象类型,则创建一个新的 Proxy 对象来代理该对象,并使用同样的代理对象 dataProxy;当程序为 data 对象的某个属性赋值时,set 方法会设置相应的属性值,并检查该属性是否有相应的监听器,如果有,则依次调用所有监听器函数,并将参数传递给它们。

接着,我们定义了一个 onChange 函数,用于向 dataProxy 对象添加监听器函数。最后,我们为 dataProxy 的两个属性 nameage 分别添加了监听器函数,当程序为 dataProxy 对象的属性赋值时,我们可以看到控制台输出了相应的信息,从而实现了一个简单的数据绑定功能。

权限控制

const user = {
  name: "Alice",
  age: 30,
  password: "123456",
};

const userProxy = new Proxy(user, {
  get(target, property, receiver) {
    if (property === "password") {
      throw new Error("Password is not accessible!");
    }
    return Reflect.get(target, property, receiver);
  },
  set(target, property, value, receiver) {
    if (property === "password") {
      throw new Error("Password cannot be changed!");
    }
    return Reflect.set(target, property, value, receiver);
  },
});

console.log(userProxy.name); // Alice
console.log(userProxy.age); // 30
console.log(() => userProxy.password); // Error: Password is not accessible!
console.log(() => userProxy.password = "1234567"); // Error: Password cannot be changed!

在使用 ProxyReflect 对象结合来实现权限控制时,我们通常会在代理对象的 getset 方法中添加相应的验证逻辑,并在需要时抛出异常。

例如,在上面的权限控制示例中,我们定义了一个 user 对象,其中包含了 nameagepassword 三个属性,然后使用 Proxy 对象 userProxy 来代理 user 对象,并在 getset 方法中添加相应的验证逻辑。具体来说,当程序尝试读取 userProxy 对象的 password 属性时,get 方法会抛出一个异常,从而禁止程序访问该属性;当程序尝试为 userProxy 对象的 password 属性赋值时,set 方法会抛出一个异常,从而禁止程序修改该属性的值。这样,我们就可以实现对 user 对象的密码属性进行权限控制的功能。

同时,我们还可以为代理对象的其他属性添加相应的验证逻辑,从而实现更加细粒度的权限控制。例如,我们可以为 userProxy 对象的 age 属性添加一个验证逻辑,要求用户必须满 18 岁才能进行修改。这样,当程序为 userProxy 对象的 age 属性赋值时,如果用户未满 18 岁,则会抛出一个异常,从而禁止程序修改该属性的值。

数据校验

const data = { name: "Alice", age: 30 };
const validators = new Map();

const dataProxy = new Proxy(data, {
  get(target, property, receiver) {
    const value = Reflect.get(target, property, receiver);
    const validator = validators.get(property);
    if (validator && !validator(value)) {
      throw new Error(`Invalid value for property ${property}!`);
    }
    return value;
  },
  set(target, property, value, receiver) {
    const result = Reflect.set(target, property, value, receiver);
    const validator = validators.get(property);
    if (validator && !validator(value)) {
      throw new Error(`Invalid value for property ${property}!`);
    }
    return result;
  },
});

function addValidator(property, validator) {
  validators.set(property, validator);
}

addValidator("name", (value) => typeof value === "string" && value.length <= 10);

console.log(() => dataProxy.name = "Bobbbbbbbbbbb"); // Error: Invalid value for property name!
console.log(dataProxy.name); // Alice

在上面的代码中,我们定义了一个 data 对象,其中包含了两个属性 nameage。然后,我们使用 Proxy 对象 dataProxy 来代理 data 对象,并定义了 getset 两个方法来拦截对 data 对象的读取和赋值操作。具体来说,当程序读取 data 对象的某个属性时,get 方法会返回相应的属性值,并检查该属性值是否符合相应的验证规则,如果不符合,则抛出一个异常;当程序为 data 对象的某个属性赋值时,set 方法会设置相应的属性值,并检查该属性值是否符合相应的验证规则,如果不符合,则抛出一个异常。

接着,我们定义了一个 addValidator 函数,用于向 dataProxy 对象添加属性验证规则。最后,我们为 dataProxy 的属性 name 添加了一个验证规则,当程序为 dataProxy 对象的属性赋值时,我们可以看到控制台输出了相应的信息,从而实现了一个简单的数据校验功能。

总结

在本文中,我们介绍了 ProxyReflect 两个对象,分别从原理、基本用法、实际应用和结合使用等方面进行了详细讲解。

值得注意的是,ProxyReflect 对象虽然非常强大,但也有一些局限性。首先,它们只能代理对象,无法代理原始类型的值。其次,它们无法代理一些内部属性,如 lengthprototype 等。此外,它们在一些特殊情况下也可能出现性能问题,如代理数组时,使用 Proxy 对象来替代原生的数组方法可能会导致性能下降。

ProxyReflect 在许多现代 JavaScript 框架和库中都得到了广泛的应用,如: VueMobx等。熟悉 ProxyReflect 对象还能帮助我们更好地理解某些框架源码的实现原理,进而开发出更加高效、灵活和可靠的应用程序。

原文链接:https://juejin.cn/post/7218487123212910650 作者:XinD

(0)
上一篇 2023年4月6日 上午10:36
下一篇 2023年4月6日 上午10:47

相关推荐

发表回复

登录后才能评论