JavaScript Proxy拦截操作实现数据校验的完整示例
在 JavaScript 开发中,直接修改对象属性(如 user.age = -5)非常常见,但也极易引入脏数据。传统的解决方案是在赋值前手动编写 if 语句进行校验,这种方式代码冗余且容易遗漏。Proxy 对象提供了一种机制,可以在操作目标对象之前进行“拦截”,从而实现统一、非侵入式的数据校验。
1. 理解 Proxy 的基本工作原理
Proxy 用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
它由两个核心部分组成:
target:要被代理的目标对象。handler:一个对象,定义了拦截行为(称为“陷阱”)。
当外部操作作用于 Proxy 实例时,handler 中的逻辑会优先执行。
以下是 Proxy 拦截属性赋值操作的执行流程:
graph LR
A[代码执行赋值] -->|proxy.name = value| B[Proxy 对象]
B -->|触发 set 陷阱| C{Handler 逻辑校验}
C -->|校验失败| D[抛出 TypeError]
C -->|校验成功| E[Reflect.set 赋值]
E --> F[Target 目标对象更新]
2. 实现基础的数据类型校验
本阶段将实现一个最简单的拦截器:确保对象的 age 属性只能是数字类型。
-
定义一个包含原始数据的对象
person。const person = { name: '张三', age: 25 }; -
创建
handler对象,并编写set陷阱函数。set方法接收四个参数:目标对象、属性名、属性值和 Proxy 实例本身。const validator = { set(target, property, value) { if (property === 'age') { if (typeof value !== 'number') { throw new TypeError('年龄必须是数字'); } } // 校验通过,执行默认赋值行为 target[property] = value; return true; // 表示赋值成功 } }; -
实例化
Proxy对象,将person和validator传入。const proxyPerson = new Proxy(person, validator); -
测试赋值操作。
- 执行合法赋值:
proxyPerson.age = 26; console.log(person.age); // 输出: 26 - 执行非法赋值:
proxyPerson.age = '三十岁'; // 抛出错误: Uncaught TypeError: 年龄必须是数字
- 执行合法赋值:
3. 构建通用且健壮的校验器
为了解决硬编码属性名的问题,我们可以构建一个通用的校验工厂函数,通过配置对象来定义规则。
-
定义规则描述结构。我们需要一个对象来存储每个属性的校验逻辑(如类型、范围、是否必填)。
-
编写
createValidator工厂函数。function createValidator(target, rules) { return new Proxy(target, { set(target, property, value) { // 1. 检查是否有该属性的规则定义 if (!rules.hasOwnProperty(property)) { target[property] = value; return true; } const rule = rules[property]; const errors = []; // 2. 校验类型 if (rule.type && typeof value !== rule.type) { errors.push(`期望类型为 ${rule.type},实际得到 ${typeof value}`); } // 3. 校验最大值/最小值 if (rule.max !== undefined && value > rule.max) { errors.push(`值不能大于 ${rule.max}`); } if (rule.min !== undefined && value < rule.min) { errors.push(`值不能小于 ${rule.min}`); } // 4. 校验是否必填(处理空字符串或 null) if (rule.required && (value === null || value === '')) { errors.push('该属性为必填项'); } // 5. 如果有错误,抛出异常 if (errors.length > 0) { throw new Error(`校验失败 [${property}]: ${errors.join('; ')}`); } // 6. 校验通过,赋值 target[property] = value; // 可选:在这里添加副作用,如更新 UI、发送日志等 console.log(`属性 ${property} 已更新为: ${value}`); return true; } }); } -
配置具体的校验规则。
const productRules = { price: { type: 'number', min: 0, required: true }, name: { type: 'string', required: true }, stock: { type: 'number', min: 0 } }; -
应用校验器到业务数据。
const productData = { name: '机械键盘', price: 199 }; const safeProduct = createValidator(productData, productRules); -
验证边界情况。
- 尝试设置价格为负数:
safeProduct.price = -50; // 抛出错误: 校验失败 [price]: 值不能小于 0 - 尝试设置正确的库存:
safeProduct.stock = 100; // 输出: 属性 stock 已更新为: 100
- 尝试设置价格为负数:
4. 进阶:利用 get 拦截保护私有属性
除了限制赋值,我们还可以利用 get 拦截器防止读取以 _ 开头的内部私有属性。
-
修改
handler,增加get陷阱。const privateGuard = { get(target, property) { if (property.startsWith('_')) { throw new Error(`禁止访问私有属性: ${property}`); } return target[property]; }, set(target, property, value) { if (property.startsWith('_')) { throw new Error(`禁止修改私有属性: ${property}`); } target[property] = value; return true; } }; -
创建包含私有属性的对象。
const userConfig = { apiKey: '123-456', _internalId: 'sys_001' }; const guardedUser = new Proxy(userConfig, privateGuard); -
测试访问权限。
console.log(guardedUser.apiKey); // 正常输出: '123-456' console.log(guardedUser._internalId); // 抛出错误: 禁止访问私有属性: _internalId
5. 完整代码汇总
将上述逻辑整合为一个可直接运行的单文件示例。
// 校验工具函数
function createValidator(target, rules) {
return new Proxy(target, {
set(target, property, value) {
if (rules[property]) {
const rule = rules[property];
if (rule.required && (value === null || value === '')) {
throw new Error(`[${property}] 是必填项`);
}
if (rule.type && typeof value !== rule.type) {
throw new Error(`[${property}] 类型错误,期望 ${rule.type}`);
}
if (typeof value === 'number') {
if (rule.max !== undefined && value > rule.max) throw new Error(`[${property}] 超过最大值 ${rule.max}`);
if (rule.min !== undefined && value < rule.min) throw new Error(`[${property}] 小于最小值 ${rule.min}`);
}
}
target[property] = value;
return true;
}
});
}
// 定义数据与规则
const gameData = {
level: 1,
score: 0
};
const gameRules = {
level: { type: 'number', min: 1, max: 99 },
score: { type: 'number', min: 0 }
};
// 代理对象
const proxyGame = createValidator(gameData, gameRules);
// 测试运行
try {
proxyGame.level = 10; // 成功
proxyGame.score = -5; // 失败并抛出错误
} catch (e) {
console.error(e.message);
}
console.log(proxyGame.level); // 输出 10
暂无评论,快来抢沙发吧!