~/ ?.log $
返回文章列表
8 min read
更新于 2026年3月4日

我不知道的 V8(05)— 为什么不建议使用 delete 删除属性

"delete 会破坏 V8 优化"——这个建议在前端社区流传很广,但通常没有解释背后的原因。知道"不要用"和知道"为什么不要用"是两回事。后者能帮你在真正需要用 delete 的场景里,做出更准确的判断。

delete 会破坏 V8 优化”——这个建议在前端社区流传很广,但通常没有解释背后的原因。知道”不要用”和知道”为什么不要用”是两回事。后者能帮你在真正需要用 delete 的场景里,做出更准确的判断。

一、delete 做了什么

表面上,delete obj.name 就是从对象上移除 name 属性。但在 V8 内部,这个操作的后果远不止于此。

先看正常的对象创建:

const obj = { name: 'V8', version: '8.4' };

V8 的内部状态:

隐藏类 C0:
  name    → offset 0
  version → offset 1

obj:
  隐藏类 → C0
  属性存储: [ "V8", "8.4" ]

快属性模式下,访问 obj.name 就是读数组的 offset 0,极快。

现在执行 delete obj.name

V8 执行 delete 的步骤:
1. 检查 name 属性的描述符,确认 configurable 为 true(可删除)
2. 对象当前是快属性模式:
   → 将 offset 0 的槽位标记为"空洞"(hole)
   → 当前隐藏类 C0 不再准确(它描述的属性不存在了)
3. 决策:需要新建一个隐藏类,还是直接转为字典模式?
   → 如果这是一次偶发操作,可能新建隐藏类 C1(只含 version)
   → 如果已经有过多次属性变化,直接转为字典模式

转换后:

隐藏类 C1:
  version → offset 0   ← 重新计算了 offset

obj:
  隐藏类 → C1            ← 隐藏类切换了
  属性存储: [ "8.4" ]    ← 重新整理了存储

这不是简单地”从数组里删掉一个元素”,而是整个对象的元数据都发生了变化

二、隐藏类分裂:为什么开销这么大

每次 delete 触发的隐藏类变化,会带来连锁反应。

假设代码里有 1000 个结构相同的对象,它们共享隐藏类 C0:

const users = Array.from({ length: 1000 }, (_, i) => ({
  name: `User${i}`,
  version: '8.4',
}));
// 所有 1000 个 user 对象共享隐藏类 C0
// 内联缓存 (IC) 非常高效:一次学习,1000 次复用

现在对其中一个执行 delete

delete users[0].name;
// users[0] 的隐藏类从 C0 变成 C1
// 其他 999 个 user 还是 C0
// 访问 users[0] 的代码和访问 users[1] 的代码命中不同的隐藏类

内联缓存的影响: 原本 V8 只需要学习一次 C0 的结构,就能高效处理所有 1000 个 user。delete 之后,同一个访问点(比如 for...of 循环里的 user.name)碰到了两种隐藏类(C0 和 C1),内联缓存从单态(Monomorphic,最快)退化为多态(Polymorphic,略慢)。

继续删除更多对象的属性,碰到的隐藏类越来越多,内联缓存可能退化为超多态(Megamorphic),彻底失去优化效果。

三、转为字典模式:更严重的后果

如果 delete 操作频繁,V8 会做一个更激进的决定:直接把对象转为字典模式(Dictionary Mode)

const obj = { a: 1, b: 2, c: 3 };
delete obj.a;
obj.d = 4;
delete obj.b;
obj.e = 5;
// 频繁的增删操作 → V8 判定:维持隐藏类得不偿失 → 转为字典模式

字典模式意味着:

  • 属性访问从偏移量读取变为哈希查找
  • 隐藏类优化完全失效
  • 内联缓存对字典模式对象的优化能力大幅下降
// 验证
const obj = { name: 'V8' };
console.log(%HasFastProperties(obj)); // true

delete obj.name;
console.log(%HasFastProperties(obj)); // false(可能直接进字典模式)

四、替代方案

方案一:赋值 nullundefined

保留属性的存在,只改变它的值。隐藏类不变。

// 不推荐:破坏隐藏类
delete obj.name;

// 推荐:保持隐藏类稳定
obj.name = null;
// 或
obj.name = undefined;

语义上的差异:delete"name" in objfalse;赋值 null"name" in obj 仍为 true,只是值为 null。如果代码依赖 in 操作符检查属性存在性,两种方式有行为差异。

方案二:对象解构排除属性(创建新对象)

const { name, ...rest } = obj;
// rest 是不含 name 的新对象
// 原来的 obj 不受影响,隐藏类稳定

适合需要”逻辑上移除属性”但不在乎原对象是否修改的场景。

方案三:用 Map 替代频繁增删的对象

// 频繁增删键值对 → 换 Map
const map = new Map();
map.set('key', 'value');
map.delete('key'); // Map 天生支持高效删除,不影响其他操作的性能

五、什么时候 delete 是可以接受的

并非所有 delete 都会造成显著影响。以下情况可以接受:

  • 冷路径代码:不在循环内、不在频繁执行的函数里
  • 小型、低频访问的对象:如配置对象,创建一次,访问次数有限
  • 逻辑正确性要求:代码必须依赖 in 操作符区分”属性不存在”和”属性值为 null”

真正需要避免的场景:在热点代码(循环、渲染函数、事件处理器)里对结构稳定的对象频繁使用 delete

六、总结

delete 的性能问题不是”它慢”,而是它改变了对象的隐藏类。隐藏类的改变会:

  1. 使当前对象与其他同类型对象的隐藏类不再一致,破坏内联缓存复用
  2. 频繁操作时让 V8 放弃快属性,转为字典模式,访问速度进一步下降

替代方案按优先级:

  • 赋值 null/undefined(大多数情况下足够)
  • 解构新对象(需要干净的新对象时)
  • Map(需要频繁增删键值对时)

本系列其他文章:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;