• 作者:老汪软件技巧
  • 发表时间:2024-08-20 07:01
  • 浏览量:

关于ES6(ECMAScript 2015)中的Proxy及Reflect 你真的清楚了吗?!

最近的面试发现Proxy是一个非常频繁的话题,提到Proxy自然而然的也会提及Reflect。Proxy 的一个典型用例是拦截和控制对目标对象的访问,这在开发中有很多实际应用场景的应用,在此专门做了一些日常开发中用到的,以及暂时没用到的(可能永远用不到......)的相关总结抛去之前你所认识的代理吧,从现在开始,请吊打面试官吧

Proxy

来自MDN的原文解释:Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

Proxy的使用场景1. 数据绑定与观察者模式

通过 Proxy,可以轻松实现数据绑定和响应式系统。当对象属性发生变化时,自动触发某些操作。例如,Vue.js 2.x 的响应式系统就是基于 Object.defineProperty 实现的,而 Vue 3.x 则使用了 Proxy 来增强性能和灵活性。

const handler = {
  set(target, property, value) {
    console.log(`${property} 被设置为 ${value}`);
    target[property] = value;
    return true;
  }
};
const data = new Proxy({ name: 'John' }, handler);
data.name = 'Doe';  // 控制台输出:name 被设置为 Doe

2. 输入验证

可以使用 Proxy 来拦截对对象的赋值操作,防止非法值的赋值。例如,确保数值范围在一定范围内。

const validator = {
  set(target, property, value) {
    if (property === 'age') {
      if (!Number.isInteger(value) || value <= 0) {
        throw new Error('年龄必须是正整数');
      }
    }
    target[property] = value;
    return true;
  }
};
const person = new Proxy({}, validator);
person.age = 25;  // 正常赋值
person.age = -5;  // 抛出错误:年龄必须是正整数

3. 动态属性

Proxy 可以用来创建动态属性或者计算属性的对象。这样我们可以在读取属性时动态生成其值。

const dynamicObject = new Proxy({}, {
  get(target, property) {
    return property in target ? target[property] : `Property ${property} is not defined`;
  }
});
console.log(dynamicObject.name);  // 输出:Property name is not defined

4. 保护对象

可以用 Proxy 实现私有属性的保护机制,限制某些属性只能通过特定方式访问,防止外部直接操作。

const handler = {
  get(target, property) {
    if (property.startsWith('_')) {
      throw new Error('私有属性不可访问');
    }
    return target[property];
  },
  set(target, property, value) {
    if (property.startsWith('_')) {
      throw new Error('私有属性不可修改');
    }
    target[property] = value;
    return true;
  }
};
const safeObject = new Proxy({ _private: 'secret', public: 'hello' }, handler);
console.log(safeObject.public);  // 输出:hello
console.log(safeObject._private);  // 抛出错误:私有属性不可访问

5. 对象的撤销与恢复

通过 Proxy.revocable 方法,可以创建一个可撤销的代理。这个代理可以在特定时刻被撤销,从而停止对目标对象的访问。

let { proxy, revoke } = Proxy.revocable({}, {
  get(target, property) {
    return `Property ${property}`;
  }
});
console.log(proxy.test);  // 输出:Property test
revoke();  // 撤销代理
console.log(proxy.test);  // 抛出错误:TypeError: Cannot perform 'get' on a proxy that has been revoked

6. 默认值

使用 Proxy 可以为对象的属性提供默认值。如果读取一个不存在的属性,可以返回一个默认值而不是 undefined。

const withDefault = (target, defaultValue = 0) => 
  new Proxy(target, {
    get: (obj, prop) => (prop in obj ? obj[prop] : defaultValue)
  });
const position = withDefault({ x: 10, y: 20 }, 0);
console.log(position.x);  // 输出:10
console.log(position.z);  // 输出:0

7. 属性别名

Proxy 可以用来创建属性的别名。当访问一个属性时,可以映射到另一个属性。

const withAlias = (target, aliasMap) =>
  new Proxy(target, {
    get: (obj, prop) => (prop in aliasMap ? obj[aliasMap[prop]] : obj[prop]),
    set: (obj, prop, value) => {
      if (prop in aliasMap) {
        obj[aliasMap[prop]] = value;
      } else {
        obj[prop] = value;
      }
      return true;
    }
  });
const user = withAlias({ firstName: 'John', lastName: 'Doe' }, { name: 'firstName' });
console.log(user.name);  // 输出:John
user.name = 'Jane';
console.log(user.firstName);  // 输出:Jane

8. 函数参数的灵活性

使用 Proxy 可以创建函数参数的动态处理机制,允许函数在参数不匹配时提供灵活的处理方式。

function flexibleFunction(...args) {
  const proxy = new Proxy(args, {
    get(target, prop) {
      return target[prop] || `参数 ${prop} 不存在`;
    }
  });
  return proxy;
}
const result = flexibleFunction(1, 2, 3);
console.log(result[0]);  // 输出:1
console.log(result[5]);  // 输出:参数 5 不存在

9. 保护不可变数据

可以使用 Proxy 防止对对象的修改,确保数据的不可变性。在处理数据的纯函数式编程中非常有用。

const deepFreeze = (obj) => {
  const handler = {
    set(target, prop, value) {
      throw new Error(`Cannot modify property ${prop} of a frozen object`);
    }
  };
  const proxy = new Proxy(obj, handler);
  Object.keys(obj).forEach(key => {
    if (typeof obj[key] === 'object') {
      obj[key] = deepFreeze(obj[key]);
    }
  });
  return proxy;
};
const frozenObject = deepFreeze({ name: 'John', details: { age: 30 } });
frozenObject.name = 'Jane';  // 抛出错误:Cannot modify property name of a frozen object

_清楚中国_清楚中的楚是什么意思

10. 日志记录与调试

可以通过 Proxy 拦截对对象的操作,自动记录日志或提供调试信息。例如,在开发调试阶段,自动记录所有对对象的操作。

const loggingProxy = (obj) => 
  new Proxy(obj, {
    get(target, prop) {
      console.log(`读取属性: ${prop}`);
      return target[prop];
    },
    set(target, prop, value) {
      console.log(`设置属性: ${prop} = ${value}`);
      target[prop] = value;
      return true;
    }
  });
const user = loggingProxy({ name: 'John', age: 25 });
user.name = 'Jane';  // 控制台输出:设置属性: name = Jane
console.log(user.age);  // 控制台输出:读取属性: age,结果:25

11. 虚拟属性与计算属性

可以通过 Proxy 实现虚拟属性或计算属性。例如,定义一个虚拟的 fullName 属性,它从 firstName 和 lastName 组合而来。

const person = {
  firstName: 'John',
  lastName: 'Doe'
};
const virtualProperties = new Proxy(person, {
  get(target, prop) {
    if (prop === 'fullName') {
      return `${target.firstName} ${target.lastName}`;
    }
    return target[prop];
  },
  set(target, prop, value) {
    if (prop === 'fullName') {
      const [firstName, lastName] = value.split(' ');
      target.firstName = firstName;
      target.lastName = lastName;
      return true;
    }
    target[prop] = value;
    return true;
  }
});
console.log(virtualProperties.fullName);  // 输出:John Doe
virtualProperties.fullName = 'Jane Smith';
console.log(virtualProperties.firstName);  // 输出:Jane
console.log(virtualProperties.lastName);  // 输出:Smith

Reflect

来自MDN的解释:Reflect是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与[proxy handler]法相同。Reflect不是一个函数对象,因此它是不可构造的。

描述

与大多数全局对象不同Reflect并非一个构造函数,所以不能通过对其进行调用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像对象)。

Reflect使用场景1. 与 Proxy 配合使用

Proxy 可以拦截对象的操作并执行自定义逻辑,但有时候我们想在拦截自定义逻辑后继续执行默认操作,这时可以使用 Reflect 方法。例如在 set 拦截器中,如果需要继续执行对象属性的默认赋值操作,就可以使用 Reflect.set。

const handler = {
  set(target, property, value, receiver) {
    console.log(`设置属性: ${property} = ${value}`);
    return Reflect.set(target, property, value, receiver);
  }
};
const obj = new Proxy({}, handler);
obj.name = 'John';  // 输出:设置属性: name = John
console.log(obj.name);  // 输出:John

2. 替代操作符

Reflect 提供了一些方法,替代传统的操作符和函数调用,这些方法更加直观和一致。例如:

使用 Reflect 可以统一操作接口,使代码更加易读和一致。

const obj = { name: 'John', age: 30 };
// 使用 Reflect.get 代替 obj.name
console.log(Reflect.get(obj, 'name'));  // 输出:John
// 使用 Reflect.set 代替 obj.age = 31
Reflect.set(obj, 'age', 31);
console.log(obj.age);  // 输出:31
// 使用 Reflect.has 代替 'name' in obj
console.log(Reflect.has(obj, 'name'));  // 输出:true
// 使用 Reflect.deleteProperty 代替 delete obj.name
Reflect.deleteProperty(obj, 'name');
console.log(obj);  // 输出:{ age: 31 }

3. 更安全的操作

有些操作符可能会导致意外的错误,而使用 Reflect 可以更安全地执行这些操作。例如,Reflect.set 返回一个布尔值,表示赋值操作是否成功,而直接使用赋值操作符可能会导致 TypeError。

const obj = Object.freeze({ age: 30 });
// Reflect.set 返回 false 而不是抛出异常
if (!Reflect.set(obj, 'age', 31)) {
  console.log('赋值失败,属性无法修改');
}

4. 简化对象的操作

使用 Reflect 可以更方便地操作对象,而不需要通过 Object 的方法进行转换。尤其在需要处理对象的继承关系时,Reflect 提供了比 Object 更强大的能力。

const person = {
  get greeting() {
    return 'Hello';
  }
};
// Reflect.get 的 receiver 参数允许你在继承链上指定 `this` 值
const receiver = { greeting: 'Hi' };
console.log(Reflect.get(person, 'greeting', receiver));  // 输出:Hello

5. 在 Proxy 中统一调用原始行为

使用 Proxy 时,开发者可以通过 Reflect 更简洁地调用原始行为,避免手动处理复杂的继承链或上下文问题。

const handler = {
  get(target, property, receiver) {
    console.log(`读取属性: ${property}`);
    return Reflect.get(target, property, receiver);
  }
};
const obj = new Proxy({ greeting: 'Hello' }, handler);
console.log(obj.greeting);  // 输出:读取属性: greeting,Hello

6. 替代 Function.prototype.apply 和 Function.prototype.call

Reflect.apply 提供了一种更一致的方式来调用函数,类似于 Function.prototype.apply,但语法更简洁直观。

const sum = (a, b) => a + b;
console.log(Reflect.apply(sum, null, [1, 2]));  // 输出:3

总结

Proxy 和 Reflect 是 ES6 的元编程利器。Proxy 让开发者能够以更细粒度的方式控制对象行为,Reflect 则提供了调用这些行为的标准方法。二者结合使用,可以帮助开发者实现更强大、更灵活的功能,同时保持代码的简洁和一致性。

下一篇聊聊Map、Set以及其弱引用:WeakMap、WeakSet,手写一个高效的深拷贝方法 ➡️