getElementsByClassName 是实时集合:一个循环只删掉一半元素

想清空一个列表,很自然这么写:

const items = document.getElementsByClassName('item');
for (let i = 0; i < items.length; i++) {
  items[i].remove();
}

五个元素,跑完剩两个(或者三个,取决于你在哪停)。第二次跑剩下的才清干净。

原因:这个集合是活的

getElementsByClassName 返回的是 HTMLCollection,它是对当前 DOM 的实时视图,不是快照。你删掉一个元素,集合立刻变短,后面的元素整体前移一位。

循环走到 i = 0,删掉原来的第 0 个;此时原来的第 1 个变成了新的第 0 个,而循环 i 加到 1,指向原来的第 2 个。原来的第 1 个就这样被跳过了。

三行验完,打开任意页面的 console:

const live = document.getElementsByClassName('item');  // 假设 5 个
live.length;      // 5
live[0].remove();
live.length;      // 4  ← 同一个变量,立刻变了

对照:querySelectorAll 是快照

const snap = document.querySelectorAll('.item');       // 5 个
snap.length;      // 5
snap[0].remove();
snap.length;      // 5  ← 不变,它记的是取的那一刻

一个是视图,一个是名单。这个差别决定了你能不能边遍历边改。

几个常见方法返回的类型,值得记一下:

方法 / 属性 返回 实时? 有 forEach?
querySelectorAll NodeList 否 是
getElementsByClassName HTMLCollection 是 否
getElementsByTagName HTMLCollection 是 否
element.children HTMLCollection 是 否
element.childNodes NodeList 是 是

注意最后两行:childNodes 是 NodeList 但也是实时的。所以”NodeList 就是静态的”这个说法不对——实时与否取决于怎么拿到的,不是取决于类型名。这也解释了为什么光看方法名猜不出来,测一次最省事。

四种正确写法

/* 1. 用静态集合,最省心 */
document.querySelectorAll('.item').forEach(el => el.remove());

/* 2. 倒着删,实时集合也不会跳 */
const items = document.getElementsByClassName('item');
for (let i = items.length - 1; i >= 0; i--) items[i].remove();

/* 3. 先拷成真数组 */
[...document.getElementsByClassName('item')].forEach(el => el.remove());

/* 4. 一直删第一个,直到没有 —— 最不容易想错 */
const live = document.getElementsByClassName('item');
while (live.length) live[0].remove();

第 4 种写法看着笨,但它把”长度会变”这件事当成前提而不是意外,反而最难写错。

顺带三个同族的坑

一、NodeList 有 forEach,但没有 map / filter。 它不是数组,只是恰好实现了 forEach。要映射就先转:Array.from(list).map(...) 或 [...list].map(...)。

二、const 声明的数组不是不可变的。

const a = [];
a.push(1);        // 完全合法
a.length = 0;     // 也合法
a = [2];          // ← 只有这句报错

const 锁的是”这个变量指向哪个对象”,不是对象的内容。要冻结内容得用 Object.freeze(),而且它是浅冻结——嵌套对象照样能改。

三、for...of 能遍历 HTMLCollection。 它不是数组、没有 forEach,但实现了迭代协议,所以 for (const el of collection) 是可以的。不过它仍然是实时集合,边遍历边删还是那个问题——只是换成 for...of 之后跳过的规律更难预测,所以我不这么写。

怎么一眼认出来

别背表。写完之后在 console 里删一个元素、打印一次 length,就知道手上这个是名单还是视图。这个动作十秒钟,比记五个方法的返回值可靠。

← 返回技术笔记