MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的代理(Proxy)与反射(Reflection)

2025-01-064.8k 阅读

JavaScript 中的代理(Proxy)

代理的基本概念

在 JavaScript 中,代理(Proxy)是一种用于创建对象代理的机制,它可以在目标对象之前设置一层“拦截”,通过这层拦截,我们可以对目标对象的各种操作(如读取属性、设置属性、枚举属性等)进行自定义的处理。代理对象包装了目标对象,并提供了与目标对象基本相同的接口,但是在对目标对象进行实际操作之前,会先经过代理对象的处理逻辑。

创建代理对象

使用 Proxy 构造函数来创建代理对象。Proxy 构造函数接受两个参数:目标对象(target)和处理程序对象(handler)。处理程序对象定义了在执行各种操作时代理对象的行为。以下是一个简单的示例:

const target = {
  message: 'Hello, world!'
};

const handler = {
  get(target, property) {
    return target[property].toUpperCase();
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.message); // 输出: HELLO, WORLD!

在这个例子中,我们创建了一个代理对象 proxy,它包装了目标对象 target。处理程序对象 handler 定义了 get 方法,当通过代理对象读取属性时,get 方法会被调用。这里我们将读取到的属性值转换为大写并返回。

代理的捕获器(Traps)

代理的处理程序对象包含了一系列被称为捕获器(Traps)的方法,这些方法用于拦截和自定义目标对象的各种操作。以下是一些常见的捕获器:

get 捕获器

get 捕获器用于拦截对象属性的读取操作。其语法为:

handler.get = function(target, property, receiver) {
  // 自定义逻辑
};
  • target:目标对象。
  • property:要读取的属性名。
  • receiver:操作发生时的接收者对象,通常是代理对象本身,或者继承自代理对象的对象。

例如,我们可以实现一个代理,当读取不存在的属性时返回一个默认值:

const target = {
  name: 'John'
};

const handler = {
  get(target, property) {
    if (target.hasOwnProperty(property)) {
      return target[property];
    } else {
      return 'Default value';
    }
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.name); // 输出: John
console.log(proxy.age);  // 输出: Default value

set 捕获器

set 捕获器用于拦截对象属性的设置操作。语法如下:

handler.set = function(target, property, value, receiver) {
  // 自定义逻辑
  return true; // 返回 true 表示设置成功,否则返回 false
};
  • target:目标对象。
  • property:要设置的属性名。
  • value:要设置的值。
  • receiver:操作发生时的接收者对象。

下面的例子展示了如何使用 set 捕获器来限制对某个属性的赋值:

const target = {};

const handler = {
  set(target, property, value) {
    if (property === 'password') {
      if (typeof value ==='string' && value.length >= 6) {
        target[property] = value;
        return true;
      } else {
        console.error('Password must be at least 6 characters long.');
        return false;
      }
    } else {
      target[property] = value;
      return true;
    }
  }
};

const proxy = new Proxy(target, handler);
proxy.username = 'user1';
proxy.password = '1234'; // 输出: Password must be at least 6 characters long.
proxy.password = '123456';
console.log(proxy.password); // 输出: 123456

has 捕获器

has 捕获器用于拦截 in 操作符,判断对象是否包含某个属性。语法为:

handler.has = function(target, property) {
  // 自定义逻辑
  return true; // 返回 true 表示对象包含该属性,否则返回 false
};

例如,我们可以创建一个代理,隐藏某些属性不被 in 操作符检测到:

const target = {
  name: 'Alice',
  _secret: 'hidden value'
};

const handler = {
  has(target, property) {
    if (property === '_secret') {
      return false;
    }
    return property in target;
  }
};

const proxy = new Proxy(target, handler);
console.log('name' in proxy); // 输出: true
console.log('_secret' in proxy); // 输出: false

deleteProperty 捕获器

deleteProperty 捕获器用于拦截 delete 操作符,删除对象的属性。语法如下:

handler.deleteProperty = function(target, property) {
  // 自定义逻辑
  return true; // 返回 true 表示删除成功,否则返回 false
};

下面的示例展示了如何使用 deleteProperty 捕获器来禁止删除某些属性:

const target = {
  name: 'Bob',
  age: 30
};

const handler = {
  deleteProperty(target, property) {
    if (property === 'name') {
      console.error('Cannot delete name property.');
      return false;
    }
    delete target[property];
    return true;
  }
};

const proxy = new Proxy(target, handler);
delete proxy.age;
console.log(proxy.age); // 输出: undefined
delete proxy.name; // 输出: Cannot delete name property.
console.log(proxy.name); // 输出: Bob

ownKeys 捕获器

ownKeys 捕获器用于拦截 Object.getOwnPropertyNames()Object.getOwnPropertySymbols()for...in 循环等操作,返回对象自身的属性键。语法为:

handler.ownKeys = function(target) {
  // 自定义逻辑
  return ['key1', 'key2']; // 返回属性键数组
};

例如,我们可以创建一个代理,只暴露部分属性给 for...in 循环:

const target = {
  a: 1,
  b: 2,
  c: 3
};

const handler = {
  ownKeys(target) {
    return ['a', 'b'];
  }
};

const proxy = new Proxy(target, handler);
for (const key in proxy) {
  console.log(key); // 输出: a, b
}

代理的用途

数据验证与过滤

通过代理的 set 捕获器,我们可以在设置对象属性时进行数据验证和过滤。例如,确保某个属性的值是特定类型或者在一定范围内。

const person = {};

const handler = {
  set(target, property, value) {
    if (property === 'age') {
      if (typeof value === 'number' && value >= 0 && value <= 120) {
        target[property] = value;
        return true;
      } else {
        console.error('Invalid age value.');
        return false;
      }
    } else {
      target[property] = value;
      return true;
    }
  }
};

const proxy = new Proxy(person, handler);
proxy.age = 25;
console.log(proxy.age); // 输出: 25
proxy.age = 150; // 输出: Invalid age value.

日志记录

利用代理的捕获器,我们可以记录对对象的各种操作,这对于调试和审计非常有用。

const target = {
  message: 'Initial message'
};

const handler = {
  get(target, property) {
    console.log(`Getting property: ${property}`);
    return target[property];
  },
  set(target, property, value) {
    console.log(`Setting property: ${property} to ${value}`);
    target[property] = value;
    return true;
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.message); 
// 输出: Getting property: message
// 输出: Initial message
proxy.message = 'New message'; 
// 输出: Setting property: message to New message

访问控制

代理可以用于实现访问控制,限制对对象某些属性的访问。例如,隐藏敏感信息或者限制对特定方法的调用。

const user = {
  username: 'user1',
  password: 'pass123',
  getPassword() {
    return this.password;
  }
};

const handler = {
  get(target, property) {
    if (property === 'password' || property === 'getPassword') {
      throw new Error('Access denied.');
    }
    return target[property];
  }
};

const proxy = new Proxy(user, handler);
console.log(proxy.username); // 输出: user1
// console.log(proxy.password); // 抛出错误: Access denied.
// console.log(proxy.getPassword()); // 抛出错误: Access denied.

JavaScript 中的反射(Reflection)

反射的概念

反射(Reflection)是指计算机程序在运行时能够检查和修改自身结构和行为的能力。在 JavaScript 中,反射提供了一组操作对象元数据的方法,这些方法可以在运行时检查对象的属性、方法、原型等信息,并且可以动态地操作这些对象。

Reflect 对象

JavaScript 提供了 Reflect 对象来实现反射操作。Reflect 对象包含了一系列静态方法,这些方法与代理的捕获器相对应,提供了对对象底层操作的更直接控制。

Reflect 的方法

Reflect.get()

Reflect.get() 方法用于获取对象的属性值,与通过点运算符或方括号运算符获取属性值类似,但它提供了更灵活的操作方式。语法为:

Reflect.get(target, property, receiver);
  • target:目标对象。
  • property:要获取的属性名。
  • receiver:可选参数,用于确定 this 的值,通常为 target 或继承自 target 的对象。

例如:

const obj = {
  name: 'Eve',
  greet() {
    return `Hello, ${this.name}`;
  }
};

const result = Reflect.get(obj, 'name');
console.log(result); // 输出: Eve

const greetResult = Reflect.get(obj, 'greet').call(obj);
console.log(greetResult); // 输出: Hello, Eve

Reflect.set()

Reflect.set() 方法用于设置对象的属性值,与直接使用赋值语句类似,但提供了更多的控制。语法为:

Reflect.set(target, property, value, receiver);
  • target:目标对象。
  • property:要设置的属性名。
  • value:要设置的值。
  • receiver:可选参数,用于确定 this 的值,通常为 target 或继承自 target 的对象。

示例:

const obj = {};
const success = Reflect.set(obj, 'name', 'Adam');
console.log(success); // 输出: true
console.log(obj.name); // 输出: Adam

Reflect.has()

Reflect.has() 方法用于判断对象是否包含某个属性,与 in 操作符类似。语法为:

Reflect.has(target, property);
  • target:目标对象。
  • property:要检查的属性名。

例如:

const obj = {
  age: 28
};
const hasAge = Reflect.has(obj, 'age');
console.log(hasAge); // 输出: true
const hasName = Reflect.has(obj, 'name');
console.log(hasName); // 输出: false

Reflect.deleteProperty()

Reflect.deleteProperty() 方法用于删除对象的属性,与 delete 操作符类似。语法为:

Reflect.deleteProperty(target, property);
  • target:目标对象。
  • property:要删除的属性名。

示例:

const obj = {
  color:'red'
};
const success = Reflect.deleteProperty(obj, 'color');
console.log(success); // 输出: true
console.log(obj.color); // 输出: undefined

Reflect.ownKeys()

Reflect.ownKeys() 方法用于获取对象自身的所有属性键,包括符号属性。语法为:

Reflect.ownKeys(target);
  • target:目标对象。

例如:

const obj = {
  a: 1,
  b: 2
};
const sym = Symbol('secret');
obj[sym] = 'hidden';

const keys = Reflect.ownKeys(obj);
console.log(keys); 
// 输出: [ 'a', 'b', Symbol(secret) ]

代理与反射的结合使用

代理和反射在 JavaScript 中常常结合使用,代理通过捕获器拦截对象操作,而反射提供了在捕获器中执行相应操作的方法。这样可以实现更加灵活和强大的对象操作控制。

例如,我们可以使用代理和反射来实现一个只读对象:

function makeReadOnly(target) {
  return new Proxy(target, {
    set(target, property, value) {
      console.error('Cannot set property on a read - only object.');
      return false;
    },
    deleteProperty(target, property) {
      console.error('Cannot delete property on a read - only object.');
      return false;
    }
  });
}

const data = {
  value: 42
};

const readOnlyData = makeReadOnly(data);
const setResult = Reflect.set(readOnlyData, 'value', 100);
console.log(setResult); 
// 输出: false
// 输出: Cannot set property on a read - only object.

const deleteResult = Reflect.deleteProperty(readOnlyData, 'value');
console.log(deleteResult); 
// 输出: false
// 输出: Cannot delete property on a read - only object.

在这个例子中,代理对象通过捕获器拦截了 setdeleteProperty 操作,并且在捕获器中使用 console.error 输出错误信息并返回 false,表示操作失败。同时,使用 Reflect 方法来尝试执行这些操作,展示了代理和反射的协同工作。

再比如,我们可以利用代理和反射实现一个日志记录代理:

function logProxy(target) {
  return new Proxy(target, {
    get(target, property, receiver) {
      console.log(`Getting property: ${property}`);
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      console.log(`Setting property: ${property} to ${value}`);
      return Reflect.set(target, property, value, receiver);
    }
  });
}

const myObject = {
  message: 'Hello'
};

const loggedObject = logProxy(myObject);
console.log(loggedObject.message); 
// 输出: Getting property: message
// 输出: Hello
loggedObject.message = 'World'; 
// 输出: Setting property: message to World

在这个示例中,代理的捕获器在执行实际的对象操作(通过 Reflect 方法)之前,先记录了操作信息,从而实现了日志记录的功能。

代理与反射在实际开发中的应用场景

框架开发

在 JavaScript 框架(如 Vue.js、React 等)的开发中,代理和反射可以用于实现数据响应式系统、状态管理等功能。例如,Vue.js 使用代理来监听数据的变化,并自动更新视图。通过代理的捕获器拦截数据的读取和设置操作,然后利用反射方法来实际执行这些操作,同时触发视图更新的逻辑。

数据层抽象

在构建数据层抽象时,代理和反射可以用于封装数据访问逻辑,提供统一的接口来操作不同类型的数据存储(如本地存储、服务器端 API 等)。代理可以拦截对数据的各种操作,根据不同的情况调用相应的反射方法,并将操作转发到合适的数据存储。

安全与权限控制

在需要进行安全和权限控制的应用中,代理和反射可以用于实现访问控制策略。通过代理拦截对敏感数据或方法的访问,使用反射方法来检查权限,并决定是否允许操作的执行。例如,在企业级应用中,限制某些用户对特定数据对象的读写操作。

综上所述,JavaScript 中的代理和反射为开发者提供了强大的工具,通过对对象操作的拦截和元数据的操作,可以实现许多高级功能,提升代码的灵活性、可维护性和安全性。无论是在小型项目还是大型框架的开发中,合理运用代理和反射都能带来显著的优势。