JavaScript中解构参数对象的属性安全检查及长参数列表冗余优化方案
在日常开发中,我们经常要做参数存在性检查——比如普通参数列表f(x,y)里,用typeof x就能轻松搞定。但如果函数用了解构声明,比如f({x,y}),检查属性是否存在就麻烦多了,尤其是现在浏览器新版本可能会给Object.prototype新增一些未知属性,很容易让检查逻辑出错,进而引发应用问题。
而且还有个头疼的痛点:现有方案虽然能实现检查,但当参数列表很长时,会写出一堆混乱的“面条式检查”,既低效又啰嗦(就像下面这种):
function checkProps(obj) { const { toString, age } = obj; if (Object.hasOwn(obj, 'toString')) { ... } // 每个属性都要单独检查,太繁琐 }
核心问题本质
严格来说,我们要解决的核心问题是:当Object.prototype存在未知属性时,采用何种编码模式,能避免对长参数列表中的每个参数都做重复的污染检查,同时保证参数判断的准确性?
可行解决方案
经过社区用户的贡献和整理,我找到了一种简洁的方案,既能避免逐个参数的重复检查,还能让参数名称完全不受Object.prototype的影响。这个方案把原本需要调用方承担的处理逻辑,转移到了函数定义端:
// 调用示例 f({ x:1 }); function f(arg){ // 关键操作:将传入的参数对象转为无原型的纯对象后再解构 let { toString, x, y, z } = {...arg, __proto__:null}; console.log( toString || x ); // 输出1,不会被Object.prototype的toString属性干扰 };
这个方案的核心原理很简单:通过{...arg, __proto__:null}创建一个没有原型链的纯对象,这样解构出来的属性就只会来自传入的arg本身,完全不用担心Object.prototype上的任何新增属性干扰判断,自然也不需要再逐个属性调用Object.hasOwn做检查了。
几种失败的实现分析
为了让大家更清楚为什么这个方案可行,下面列举几个常见的失败尝试,看看它们的问题出在哪:
1. 直接解构参数,未处理原型污染
function f({ toString, obj1 }){ // 如果Object.prototype上存在toString属性,这里会误判为是传入的参数属性 var existing = toString || obj1; };
问题:解构操作会顺着原型链查找属性,Object.prototype上的同名属性会被当成传入的参数属性,导致存在性判断完全失效。
2. 仅展开参数对象,未清除原型
function f(arg){ let { toString, x } = {...arg}; var existing = toString || obj1; };
问题:展开后的新对象依然默认继承Object.prototype,原型上的未知属性还是会被解构出来,起不到防污染的作用。
3. 给解构属性设置默认值
function f(arg){ let { toString=null, obj1=null }={...arg}; console.log( toString || obj1 ); };
问题:默认值只有在属性完全不存在时才会生效,但如果Object.prototype上有同名属性,解构出来的是原型上的值,不会触发默认值,依然会出现误判。
4. 直接给解构参数设置默认值
function f({ toString=null, obj1=null }){ console.log( toString || obj1 ); };
问题:和上面的情况一样,原型链上的属性会被优先解构出来,设置的默认值根本起不到预期的兜底作用。
欢迎大家提出优化建议,一起完善这个方案!
内容的提问来源于stack exchange,提问作者Bitlab

