JavaScript Proxy
什么是 Proxy?
在 JavaScript 中,Proxy(代理)是 ES6(ECMAScript 2015)引入的一个强大特性,它允许你创建一个对象的代理,从而可以拦截并自定义对象的基本操作,如属性查找、赋值、枚举、函数调用等。
简单来说,Proxy 对象包装另一个对象,并拦截对这个对象的操作,让你可以在这些操作执行前后添加自定义行为。
备注
Proxy 是一种元编程特性,允许你改变 JavaScript 对象的基本行为。
基本语法
创建一个 Proxy 对象非常简单:
javascript
const proxy = new Proxy(target, handler);
其中:
target
是要代理的目标对象handler
是一个包含"陷阱"(traps)的对象,这些陷阱定义了拦截操作的行为
常用陷阱(Traps)
Proxy 支持多种陷阱,以下是一些常用的:
get
: 属性读取操作的陷阱set
: 属性设置操作的陷阱has
:in
操作符的陷阱deleteProperty
:delete
操作符的陷阱apply
: 函数调用操作的陷阱construct
:new
操作符的陷阱
实例演示
基础示例:属性访问监控
这个例子展示了如何使用 get
和 set
陷阱监控对象属性的访问:
javascript
const person = {
name: "张三",
age: 30
};
const personProxy = new Proxy(person, {
get(target, property) {
console.log(`有人正在获取 ${property} 属性的值`);
return target[property];
},
set(target, property, value) {
console.log(`有人正在设置 ${property} 属性的值为 ${value}`);
target[property] = value;
return true; // 在严格模式下,set 操作需要返回 true 表示成功
}
});
// 使用代理对象
console.log(personProxy.name); // 访问 name 属性
personProxy.age = 31; // 修改 age 属性
输出:
有人正在获取 name 属性的值
张三
有人正在设置 age 属性的值为 31
数据验证
使用 Proxy 可以轻松实现数据验证功能:
javascript
const validator = {
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number') {
throw new TypeError('年龄必须是一个数字');
}
if (value < 0 || value > 120) {
throw new RangeError('年龄必须在 0-120 之间');
}
}
// 默认行为
target[property] = value;
return true;
}
};
const person = new Proxy({}, validator);
// 正确设置
person.age = 30;
console.log(person.age); // 30
try {
// 不正确的类型
person.age = "三十";
} catch (e) {
console.error(e.message); // 年龄必须是一个数字
}
try {
// 超出范围
person.age = 150;
} catch (e) {
console.error(e.message); // 年龄必须在 0-120 之间
}
实际应用场景
1. 表单验证
javascript
const formValidator = {
required(value) {
return value !== null && value !== '';
},
minLength(value, length) {
return value.length >= length;
},
isEmail(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
};
const formData = new Proxy({}, {
set(target, property, value) {
// 特定字段的验证规则
if (property === 'email' && !formValidator.isEmail(value)) {
throw new Error('请输入有效的电子邮箱');
}
if (property === 'password' && !formValidator.minLength(value, 8)) {
throw new Error('密码长度至少为8个字符');
}
target[property] = value;
return true;
}
});
// 使用表单验证
try {
formData.email = "invalid-email";
} catch (e) {
console.error(e.message); // 请输入有效的电子邮箱
}
try {
formData.password = "1234";
} catch (e) {
console.error(e.message); // 密码长度至少为8个字符
}
2. 数据绑定(类似 Vue 的响应式系统)
一个简单的响应式系统演示:
javascript
function makeReactive(obj) {
const observers = new Map();
return new Proxy(obj, {
set(target, property, value) {
const oldValue = target[property];
target[property] = value;
// 通知所有观察者
if (observers.has(property)) {
observers.get(property).forEach(callback =>
callback(value, oldValue)
);
}
return true;
},
// 添加观察者方法
deleteProperty(target, property) {
if (property === 'observe') return false;
delete target[property];
return true;
}
});
}
const data = makeReactive({
message: "Hello",
count: 0
});
// 添加观察者
function observe(obj, property, callback) {
if (!obj.__observers) {
obj.__observers = new Map();
}
if (!obj.__observers.has(property)) {
obj.__observers.set(property, []);
}
obj.__observers.get(property).push(callback);
}
// 监听数据变化
observe(data, 'message', (newValue, oldValue) => {
console.log(`message 从 ${oldValue} 变为 ${newValue}`);
});
data.message = "Hello World"; // 触发观察者回调
输出:
message 从 Hello 变为 Hello World
3. 缓存代理
使用 Proxy 实现函数结果缓存:
javascript
function createCacheProxy(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
// 将参数数组转换为字符串作为缓存键
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('从缓存中获取结果');
return cache.get(key);
}
const result = target.apply(thisArg, args);
cache.set(key, result);
console.log('计算新结果并缓存');
return result;
}
});
}
// 一个耗时的计算函数
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 创建带缓存的代理函数
const cachedFib = createCacheProxy(fibonacci);
console.time('first call');
console.log(cachedFib(30)); // 第一次调用,计算结果
console.timeEnd('first call');
console.time('second call');
console.log(cachedFib(30)); // 第二次调用,从缓存获取
console.timeEnd('second call');
Proxy 与 Reflect
ES6 同时引入了 Reflect
对象,它提供了与 Proxy 处理程序相对应的方法,使用 Reflect
可以更优雅地在 Proxy 处理程序中调用默认行为:
javascript
const person = {
name: "李四",
age: 25
};
const personProxy = new Proxy(person, {
get(target, property, receiver) {
console.log(`获取 ${property} 属性`);
// 使用 Reflect.get 而不是 target[property]
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`设置 ${property} 属性为 ${value}`);
// 使用 Reflect.set 而不是 target[property] = value
return Reflect.set(target, property, value, receiver);
}
});
personProxy.name = "王五";
console.log(personProxy.name);
提示
使用 Reflect
可以让代码更加清晰且不必担心处理 this
等上下文问题。
Proxy 的局限性
虽然 Proxy 很强大,但也有一些限制:
- 性能影响 - 比直接操作对象会有轻微的性能开销
- 不能代理所有类型 - 某些原始类型(如字符串、数字)不能直接被代理
- 无法代理私有字段 - ES2022 引入的私有字段(#field)无法通过 Proxy 拦截
- 兼容性问题 - IE 11 及以下版本不支持 Proxy,且 Proxy 无法被 polyfill
总结
JavaScript Proxy 是一个强大的特性,它让我们可以:
- 拦截并自定义对象的基本操作
- 实现数据验证和格式化
- 创建响应式系统
- 实现缓存、日志记录等功能
- 构建更安全的对象访问机制
掌握 Proxy 可以帮助你编写更灵活、更强大的 JavaScript 代码,特别是在构建框架或库时。
练习题
- 使用 Proxy 创建一个只读对象,任何设置属性的尝试都会被拒绝。
- 实现一个自动记录所有属性访问的日志系统。
- 创建一个数组代理,当访问超出范围的索引时返回一个默认值而不是
undefined
。 - 使用 Proxy 实现一个简单的表单验证系统,验证不同类型的输入(邮箱、手机号、必填字段等)。
进一步学习资源
通过学习和使用 Proxy,你将能够创建更智能、更灵活的 JavaScript 应用程序!