为何先调用classList.contains再执行classList.remove比直接调用remove效率更高?
这个问题确实挺反直觉的——毕竟不管是多年前的Stack Overflow高赞回答,还是你之前印象里的MDN说法,都觉得先判断再删除是多此一举的冗余操作,但你实际跑出来的性能测试结果却明明白白显示:先做contains检查再调用remove,速度是直接调用remove的3倍左右,我来给你拆解下背后的原因。
首先得打破一个认知误区:classList.remove并不是简单的「contains判断 + 存在则删除」的组合。浏览器对这两个方法的底层实现,可能完全是两条不同的路径:
1. contains的快速查询依赖更轻量的内部数据结构
浏览器为了快速处理类名查询,会把元素的类名提前解析成一个可快速检索的内部集合(比如哈希表、有序数组甚至是位掩码)。contains方法直接操作这个预解析的集合,判断类名是否存在的操作是O(1)或者接近O(1)的,几乎没有额外开销。
你提到querySelectorAll用了高度优化的选择器匹配,classList.contains确实可能复用了类似的底层优化逻辑——它不需要触碰到实际的DOM属性修改,只是做一次内部的集合查询。
2. remove哪怕没找到类名,也会走完整的修改流程
而classList.remove就不一样了:哪怕目标类名不存在,它也会走一遍「尝试修改DOM属性→同步内部缓存→标记可能的渲染更新」的完整流程。这些步骤的开销比单纯的查询大得多:
- 它可能会尝试更新元素的
className属性(哪怕最终没变化) - 会触发浏览器内部的一些状态检查,比如这个元素的类名变化是否会影响样式规则匹配
- 甚至会做一些冗余的缓存一致性校验,这些都是
contains不需要做的
举个生活化的类比:contains就像是查手机通讯录有没有某个人的名字,几毫秒就能搞定;而remove是哪怕通讯录里没有这个人,你也要走一遍「点击删除→确认删除→更新最近联系人列表」的完整流程,这额外的步骤自然就拖慢了速度。
关于老回答和新测试结果的矛盾
那个9年前的高赞回答提到:
Explicitly checking is pointless. It is a waste of time and bloats your code.
其实也不能说它错——因为早期浏览器的classList实现和现在完全不同。9年前的浏览器可能真的是remove内部直接复用contains的逻辑,没有多余开销;但随着浏览器引擎的优化,现在的实现为了让contains更快,单独做了针对性优化,反而让「无意义的remove调用」(即类名不存在时的调用)产生了额外的性能损耗。
最后聊下实际开发的建议
当然,日常开发里这种性能差异大部分时候感知不到——除非你要操作成千上万的DOM元素,不然这点时间差用户完全感觉不出来。但如果是做大型列表、高频DOM操作的场景(比如数据可视化、无限滚动列表),提前用contains判断一下确实能省点性能。
给你再贴一下测试里的两种对比代码,方便大家直观参考:
直接调用remove的写法:
const c = p.children; const l = c.length; for (let i = 0; i < l; i++) { c[i].classList.remove('sel'); }
先判断再删除的写法:
const c = p.children; const l = c.length; for (let i = 0, cL; i < l; i++) { cL = c[i].classList; if( cL.contains('sel') ) cL.remove('sel'); }
内容来源于stack exchange

