迭代JavaScript DOMTokenList删除类名时出现异常行为的原因
问题根源
DOMTokenList 是实时更新的类数组集合,迭代过程中如果直接修改集合本身(增删成员),会实时改变集合的内部索引、长度属性,最终导致迭代跳过部分成员,这就是testA、testB始终残留类名b的核心原因。
失败场景执行逻辑拆解
初始状态下元素的classList为['a', 'b', 'c'],长度为3,索引0对应a、索引1对应b、索引2对应c。不管是forEach遍历,还是拿values()迭代器用for...of遍历,本质都是按索引从0开始逐位递增访问成员:
- 第一轮访问索引0,拿到值
a,执行remove('a')后classList实时刷新为['b', 'c'],长度变为2——原来在索引1的b现在偏移到索引0,原来在索引2的c偏移到索引1 - 第二轮迭代索引自动加1变成1,直接取当前索引1的值,拿到
c,执行remove('c')后classList刷新为['b'],长度变为1 - 下一轮迭代判断当前索引2已经大于集合最大索引0,直接终止遍历。全程没有访问到现在停在索引0的
b,所以最终会残留这个类。
testC 为什么能正常运行
testC的第一轮遍历只做读取操作,没有修改classList,把当时集合里的所有类名完整拷贝到了一个独立的普通数组classNames里。这个数组是静态的,后续遍历这个静态数组执行删除操作时,数组本身不会因为classList的变动发生索引偏移、长度变化,所以能把所有提前拷贝好的类名全部删除干净。
这类问题不是DOMTokenList独有:所有DOM实时集合(比如
getElementsByClassName、getElementsByTagName返回的HTMLCollection)都存在相同特性。如果需要在遍历实时集合时增删成员,要么先把集合拷贝为静态普通数组再操作,要么从集合末尾倒序向前遍历,避免索引偏移导致的漏遍历。
测试代码
function testA() { const element = document.getElementById('foo') const elementClassList = element.classList elementClassList.forEach(function(key) { elementClassList.remove(key) }) console.log(elementClassList) } function testB() { const element = document.getElementById('bar') const elementClassList = element.classList const iterator = elementClassList.values() for (let value of iterator) { elementClassList.remove(value) } console.log(elementClassList) } function testC() { const element = document.getElementById('bat') const elementClassList = element.classList let classNames = [] elementClassList.forEach(function(names) { classNames.push(names) }) classNames.forEach(function(name) { elementClassList.remove(name) }) console.log(elementClassList) }
div { border: solid 1px gray; background: #F8F9FA; padding: 1rem; margin-bottom: 1rem; }
<!doctype html> <html lang="de"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>DOMTokenList mystery</title> </head> <body> <main> <div id="foo" class="a b c"> #Foo<br> CSS-classes: a b c </div> <div id="bar" class="a b c"> #Bar<br> CSS-classes: a b c </div> <div id="bat" class="a b c"> #Bat<br> CSS-classes: a b c </div> <hr> <button onclick="testA()" type="button">testA()</button> <button onclick="testB()" type="button">testB()</button> <button onclick="testC()" type="button">testC()</button> </main> </body> </html>
内容的提问来源于stack exchange,提问作者trippodi
相关产品推荐
相关产品推荐

