Proxy能拦截所有赋值操作,而Object.defineProperty的setter仅对已知属性生效;校验逻辑必须放在set trap中,以覆盖新增属性和已有属性的修改。

Proxy 为什么能拦截赋值,而不是直接用 setter?
因为 Object.defineProperty 的 set 只对已知属性生效,新增属性或动态键(比如 obj[key] = val)完全绕过;而 Proxy 的 set trap 能捕获所有赋值操作,包括 obj.x = 1、obj['y'] = 'str'、甚至 Reflect.set(obj, key, val)。
但要注意:set trap 不会拦截原型链上的赋值(如给 obj.__proto__.z = 2),也不拦截不可配置属性的修改失败(此时返回 false 但不抛错,需手动检查)。
校验逻辑该放在 set 还是 defineProperty?
必须放在 set trap 内部。因为校验目标是“赋值行为本身”,不是“属性定义”。如果在 defineProperty trap 里做,就漏掉了所有已存在属性的修改,比如 user.age = -5 根本不会触发 defineProperty。
实操建议:
-
set中先调用校验函数,校验失败直接throw或返回false(后者会让赋值静默失败,不推荐) - 校验函数应接收
key、https://www.php.cn/link/28b1723af782c5ebb1f6522d19c6df31、receiver(当前代理对象),便于做上下文判断(比如区分user.name和user.profile.name) - 避免在
set里递归创建新 Proxy——比如对嵌套对象自动包装,容易引发无限代理或内存泄漏
常见非法数据场景与校验写法
校验不是一刀切,得按字段语义来。比如 email 要正则,age 要数字范围,tags 要数组且元素非空字符串:
const validator = {
email: (v) => typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
age: (v) => Number.isInteger(v) && v >= 0 && v <= 150,
tags: (v) => Array.isArray(v) && v.every(t => typeof t === 'string' && t.trim())
};
<p>const handler = {
set(target, key, https://www.php.cn/link/28b1723af782c5ebb1f6522d19c6df31, receiver) {
if (validator[key] && !validator<a href="https://www.php.cn/link/28b1723af782c5ebb1f6522d19c6df31">key</a>) {
throw new TypeError(<code>Invalid https://www.php.cn/link/28b1723af782c5ebb1f6522d19c6df31 for ${key}: ${https://www.php.cn/link/28b1723af782c5ebb1f6522d19c6df31}</code>);
}
return Reflect.set(target, key, https://www.php.cn/link/28b1723af782c5ebb1f6522d19c6df31, receiver);
}
};注意点:
- 校验函数不要用
typeof https://www.php.cn/link/28b1723af782c5ebb1f6522d19c6df31 === 'number'判数字——new Number(42)是 object,'123'是 string;优先用Number.isInteger、parseFloat等明确意图的判断 - 如果字段允许
null或undefined,必须显式写进校验逻辑,否则会被当成非法 - 对 Date、RegExp 等特殊对象,
instanceof在跨 iframe 场景会失效,建议用Object.prototype.toString.call(https://www.php.cn/link/28b1723af782c5ebb1f6522d19c6df31)
Proxy 赋值拦截的隐藏限制
它不能拦截所有“看起来像赋值”的操作:
-
Object.assign(target, { x: 1 }):只触发set,但若target是普通对象而非代理,就完全绕过——必须确保target本身就是 Proxy 实例 -
for...in+ 赋值、解构赋值({ x } = obj)不触发set,它们是读取行为;只有右侧是代理、左侧是变量时才涉及读取拦截 - 性能敏感场景慎用:每次赋值都进 JS 函数调用栈,比原生赋值慢 3–10 倍;高频更新字段(如动画帧中的坐标)建议先批量校验再一次性赋值
最易被忽略的一点:Proxy 拦截的是“对代理对象的赋值”,不是“对原始数据的约束”。如果把代理后的对象传给第三方库(比如 Vue 3 的 reactive 或 Redux Toolkit 的 createSlice),那些库内部可能直接操作原始对象,导致你的校验彻底失效。

















