- 教程
- 文档
【免费下载链接】30-seconds-of-code
Coding articles to level up your development skills
在 JavaScript 中,对象的属性往往需要被动态增删——比如表单数据清洗、防止敏感字段被序列化传出、或是在状态更新时剔除不再需要的字段。本文围绕 30-seconds-of-code 仓库中的 remove-object-property.md 这篇核心文档,系统讲解移除对象属性的三种主流做法:赋值undefined、使用delete运算符、以及基于解构与 rest 语法创建新对象,并结合仓库内其他相关文档与源码,帮你理清各自的语义差异、性能影响与适用场景,做到按需选型、不踩坑。
为什么需要“移除”对象属性
在 JavaScript 中,“移除属性”看似简单,实则包含两种截然不同的诉求:
- 让属性不再被读取到:属性仍然存在,只是值为
undefined,逻辑上等效于“无值”; - 让属性彻底消失:从对象中真正删除该键,
Object.keys()、for...in、序列化等操作都不会再看到它。
这两种诉求对应着完全不同的实现手段和后果,而选择错误的方案往往会造成隐蔽的 bug。下面按 30-seconds-of-code 文档的脉络,逐一分析三种可行方案。
方案一:将属性设置为undefined
最简单直接的做法是把属性值赋为undefined:
const pet = { species: 'dog', age: 3, name: 'celeste', gender: 'female' }; pet.gender = undefined; Object.keys(pet); // ['species', 'age', 'name', 'gender']如示例所示,赋值undefined之后,gender键依然存在于对象中,Object.keys()依然会将其列出。因此这种方案只改变了属性的值,并没有移除属性本身,它通常只适用于“按值或真值判断属性,而不关心属性是否存在”的场景。
从仓库中的 detect-undefined-object-property.md 可以看到一个相关隐患:直接比较obj.prop === undefined无法区分“属性存在但值为 undefined”和“属性根本不存在”,要区分这两种情况必须借助Object.prototype.hasOwnProperty():
const hasUndefinedProperty = (obj, prop) => obj.hasOwnProperty(prop) && obj[prop] === undefined; const obj = { prop: undefined }; hasUndefinedProperty(obj, 'prop'); // true hasUndefinedProperty(obj, 'porp'); // false换句话说,如果你的下游代码用Object.keys()或hasOwnProperty()判断字段是否存在,那么“赋 undefined”的方案会让你误判,这是它最大的局限。此外它同样会修改原对象(一种原地变更),若对象被多处引用,副作用会向外扩散。
方案二:使用delete运算符
delete是 JavaScript 中从语义上“彻底删除属性”的标准手段:
const pet = { species: 'dog', age: 3, name: 'celeste', gender: 'female' }; delete pet.gender; Object.keys(pet); // ['species', 'age', 'name']与方案一不同,delete会真正把gender键从对象中移除,之后Object.keys()、for...in、JSON.stringify()都不会再看到它。代价是它同样会变更原对象。
delete运算符的底层语义
从语言规范看,delete有几个容易被忽略的行为,理解它们才能避免误用:
- 只对自有属性有效:
delete无法删除原型链上继承而来的属性。若对象自身没有该键,delete返回true但什么都不发生。 - 返回布尔值:成功删除返回
true,失败返回false。在非严格模式下删除不可配置的属性会静默失败并返回false,而在严格模式下会直接抛出TypeError。 - 不可配置属性无法删除:通过
Object.defineProperty()定义且configurable: false的属性,delete是删不掉的。这与仓库中 property-enumerability.md 讨论的属性描述符体系同源——configurable、enumerable等描述符共同决定了一个属性能否被删除、能否被枚举。
从实践角度看,还有一个众所周知的引擎层面的考量:对高频访问的“热”对象反复使用delete,可能让 V8 等引擎放弃对该对象的隐藏类(hidden class)优化,从而带来一定的性能损耗。因此,若只是偶尔清洗一次数据,delete完全没问题;但如果是在性能敏感的内层循环里反复增删同一对象的属性,则更推荐下文方案三的“重建对象”思路。
方案三:解构 + rest 语法,创建不含目标属性的新对象
利用 ES6 的解构赋值与 rest 语法,可以“挑出”要移除的属性,把其余属性收集到一个新对象中:
const pet = { species: 'dog', age: 3, name: 'celeste', gender: 'female' }; const { gender, ...newPet } = pet; Object.keys(pet); // ['species', 'age', 'name', 'gender'] Object.keys(newPet); // ['species', 'age', 'name']这是三种方案中唯一不修改原对象的做法:原对象pet保持原样,newPet是排除了gender的全新对象。它的另一个突出优势是可以一次性移除多个属性:
const { gender, age, ...newPet } = pet; Object.keys(newPet); // ['species', 'name']背后的语法与语义细节
- 解构赋值是 ES6 引入的对象取值语法,配合 rest 模式可将剩余键打包成新对象,详见仓库中的 destructuring-assignment.md;
- 展开语法(spread)与 rest 语法外观相似、职责相反:spread 把对象的可枚举自有属性“展开”进新对象,rest 把“剩余的键”收拢进新对象,详见 spread-rest-syntax.md;
- 需要注意:spread/rest 对对象是浅拷贝。它只会复制可枚举的自有属性(property-enumerability.md 佐证了这一点),且嵌套的对象/数组仍然是原引用,并不会被深拷贝。若对象内部还有深层结构,需要结合 shallow-deep-clone-object.md 中介绍的
structuredClone()等方案做深拷贝。
将“解构移除”封装成通用工具
解构写法在移除单个或少数属性时很直观,但属性多了之后代码冗长、可读性下降。仓库中的 filter-object-keys.md 给出了更工程化的封装——用Object.entries()取出键值对数组,配合Array.prototype.filter()过滤,再用Object.fromEntries()还原成对象:
const omit = (obj, arr) => Object.fromEntries(Object.entries(obj).filter(([k]) => !arr.includes(k))); const obj = { a: 1, b: '2', c: 3 }; omit(obj, ['b']); // { a: 1, c: 3 }对应的“只保留”版本pick只需把条件取反;如果希望按值或按更复杂的条件过滤,还可以把数组换成谓词函数,实现pickBy/omitBy。这套组合拳与本文的解构方案殊途同归:都不变更原对象,返回全新对象,非常适合函数式编程风格下的数据清洗场景。
三种方案横向对比与选型
| 对比维度 | 赋值undefined | delete运算符 | 解构 + rest |
|---|---|---|---|
| 属性是否真正移除 | 否,键仍存在 | 是 | 是(新对象中不存在) |
| 是否修改原对象 | 是(原地变更) | 是(原地变更) | 否(返回新对象) |
Object.keys()是否仍包含该键 | 是 | 否 | 原对象是,新对象否 |
| 能否批量移除多个属性 | 需逐个赋值 | 需逐个delete | 一次可移除多个 |
| 对嵌套属性的处理 | 无特殊处理 | 无特殊处理 | 浅拷贝,需自行深拷贝 |
| 典型场景 | 仅按值判断、不关心键是否存在 | 需要彻底删除、可接受原地变更 | 状态更新、函数式编程、需要保留原对象 |
选型建议可以概括为:
- 只想让“值”失效(如清空某个字段但不影响键枚举)→ 赋值
undefined; - 必须彻底删除键、且不介意修改原对象(如清理临时字段)→
delete; - 不想动原对象、追求不可变性,或需要一次剔除多个属性(如 React/Vue 状态更新)→ 解构 + rest,或封装为
omit工具函数。
其中“不可变性”的思路与仓库中的 immutability.md 一脉相承:与其原地修改数据,不如基于旧数据创建新数据,从而减少难以追踪的副作用。
移除后的存在性校验
无论选用哪种方案,移除之后通常还需要确认结果是否符合预期。仓库中的 object-has-value-or-key.md 提供了现成的检查手段:
const hasKey = (obj, key) => Object.keys(obj).includes(key); hasKey(newPet, 'gender'); // false对于嵌套路径的检查,可以递归逐层用Object.prototype.hasOwnProperty()判断:
const hasKeyDeep = (obj, keys) => { return ( keys.length > 0 && keys.every(key => { if (typeof obj !== 'object' || !obj.hasOwnProperty(key)) return false; obj = obj[key]; return true; }) ); };把“移除”和“校验”组合起来,就能写出健壮的数据清洗逻辑:先按需移除字段,再断言敏感字段确实已不存在,最后再交给序列化或传输环节。
小结
移除 JavaScript 对象属性,本质是在“改值”与“删键”、在“原地变更”与“重建对象”之间做权衡:
- 赋值
undefined只是让值失效,键仍存在,适合按值判断的场景; delete语义上真正删除键,但会修改原对象,且受属性描述符(不可配置属性)约束;- 解构 + rest不修改原对象、可批量移除,是追求不可变性的首选,也可进一步封装为
omit/omitBy等通用工具函数。
三者各有适用边界,没有绝对的“最优解”。结合本文给出的对比表与仓库内相关文档(filter-object-keys.md、property-enumerability.md、shallow-deep-clone-object.md 等)进一步阅读,即可在真实项目中做出准确选择。
- 教程
- 文档
【免费下载链接】30-seconds-of-code
Coding articles to level up your development skills
相关推荐
Video2X 免费 4K 完整指南:本地一键视频超分与补帧
Video2X 免费 4K 完整指南:本地一键视频超分与补帧 Video2X 是一款免费、开源的机器学习视频超分辨率与补帧框架:它让 AI 模型在你自己的电脑上
教程文档30 Seconds of Code 实战:JavaScript 移除数字尾随零的三种方案与原理剖析
30 Seconds of Code 实战:JavaScript 移除数字尾随零的三种方案与原理剖析 在使用 Number.prototype.toFixed
教程文档30 seconds of code 实战指南:用 JavaScript 移除 HTML 元素属性的完整方案
30 seconds of code 实战指南:用 JavaScript 移除 HTML 元素属性的完整方案 导读 HTML 元素的属性(attribute)管
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考