想清空一个列表,很自然这么写:
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,就知道手上这个是名单还是视图。这个动作十秒钟,比记五个方法的返回值可靠。