- 作者:老汪软件技巧
- 发表时间: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,手写一个高效的深拷贝方法 ➡️