从DOM中删除HTMLCollection元素

26

我有一组段落元素。其中一些为空,一些仅包含空格,而另一些则包含内容:

<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
<p></p>
<p> </p>
<p>    </p>
<p>&nbsp;</p>
<p> &nbsp;</p>

我正在使用 getElementsByTagName 来选择它们:

var paragraphs = document.getElementsByTagName('p');
这将返回文档中的所有段落。我想要删除它们,因此我希望运行:

for (var i = 0, len = paragraphs.length; i < len; i++) {
   paragraphs[i].remove();
}

但是我会收到Uncaught TypeError: Cannot read property 'remove' of undefined的错误。我认为这很奇怪,但我想试着添加一个保护条件,看看会发生什么:

for (var i = 0, len = paragraphs.length; i < len; i++) {
   paragraphs[i] && paragraphs[i].remove();
}

没有错误,但并非所有元素都被删除。所以我再次运行它,它会移除一些之前未被移除的元素我再次运行它,最终所有段落都从文档中删除了。

我想知道我错过了什么明显的细节。

问题演示

5个回答

54

问题在于段落是一个实时列表。通过删除一个p元素,您也正在更改该列表。简单的解决方法是以倒序迭代列表:

for (var i = paragraphs.length - 1; i >= 0; --i) {
  paragraphs[i].remove();
}

另一种解决方案是创建一个静态列表(非实时列表)。您可以通过以下方式之一实现此操作:

  • 将列表转换为Array

  • var paragraphs =
      Array.prototype.slice.call(document.getElementsByTagName('p'), 0);
    
  • 使用document.querySelectorAll

  • var paragraphs = document.querySelectorAll('p');
    

然后,您可以按常规顺序(使用 for 循环)迭代列表:

for (var i = 0; i < paragraphs.length; ++i) {
  paragraphs[i].remove();
}

或者(使用for...of循环):

for (var paragraph of paragraphs) {
  paragraph.remove();
}

请注意,.remove 是一个相对较新的 DOM 方法,不是每个浏览器都支持。有关更多信息,请参见 MDN 文档


为了说明问题,让我们假设我们有一个包含三个元素的节点列表,paragraphs = [p0, p1, p2]。当您遍历该列表时会发生以下情况:

i = 0, length = 3, paragraphs[0] == p0  => paragraphs = [p1, p2]
i = 1, length = 2, paragraphs[1] == p2  => paragraphs = [p1]
i = 2, length = 1, END

因此,在这个示例中,p1没有被删除,因为它被跳过了。


太棒了。我有点想知道这个,但是绕过它的方式对我来说并不明显。感谢您提供“remove”兼容性提示。幸运的是,我的受众将在Chrome或最新的Safari上。 - Marc Kline
关于操作标题仅删除空元素的问题,添加一个条件:if (!e.hasChildNodes() || (!e.innerHTML.indexOf("&nbsp")>0))... 演示:http://jsfiddle.net/abhitalks/a2Hbn/ - Abhitalks
@FelixKling 在发布之前,我考虑过尝试其他替代方案,但我很高兴我没有这样做,因为虽然我感到有点傻没有发现它,但我不太可能在以后重复类似的错误。你的答案非常清晰,希望能对其他人有所帮助。 - Marc Kline
@abhitalks,实际上我想要删除所有项目,就像我所描述的问题一样,所以我的标题有误导性,与实际问题不符。我刚刚更新了它,使其更准确,谢谢。 - Marc Kline
2
你的双表达式for循环非常简洁,但我必须指出它会在第一次迭代时递减,条件依赖于递减的假值评估为0。在这个问题上,它会删除最后一个元素,即索引为0的元素吗?对于那些需要的人,传统的向后计数语句是:for (var i = paragraphs.length - 1; i >= 0; i--) - arichards
谢谢,运行顺畅。有一件事我不明白,你提到的“live list”是什么意思? - Mejan

10

当您移除一个项目时,HTMLCollection的长度会发生变化。一种方法是使用while循环来实现。

while(paragraphs.length > 0) {
   paragraphs[0].remove();
}

2
只有在您保证删除所有子节点时,此方法才能正常工作。 - vsync

5

为什么这对您无效:

HTMLCollection在您删除节点时会发生变化,长度与HTMLCollection数组的“真实”长度不同步。

假设您有一个包含两个DOM节点的数组,并对其进行迭代。它应该迭代两次。下面的演示非常清晰易懂:

第一次迭代 - 删除第一个节点,然后 i 增加。
第二次迭代 - 现在 i 等于 1,但是此时 paragraphs.length 也是 1,因为此时只剩下一个段落。

这导致了一个不可能的情况,即一个长度为1的数组被要求访问位置1的项,而唯一可用的位置是0(因为数组从位置0开始…)。

访问在数组(或类似于数组的对象HTMLCollection)中不存在的位置是非法的。

var paragraphs = document.getElementsByTagName('p')

for (var i = 0; i <= paragraphs.length; i++) {
   console.log(i, paragraphs.length)
   paragraphs[i].remove()
}
<p>1</p>
<p>2</p>

可能的解决方案: 延迟节点的移除

在下面的示例中,节点的移除是在完成所有迭代周期后进行的(setTimeout 延迟代码执行),关键是利用第三个参数并将将要被缓存的节点作为超时回调函数的参数传递:

var paragraphs = document.getElementsByTagName('p')

for (var i = 0, len = paragraphs.length; i < len; i++) {
   setTimeout(node => node.remove(),0 , paragraphs[i])
}
<p>Pellentesque habitant....</p>
<p></p>
<p> </p>
<p>    </p>
<p>&nbsp;</p>
<p> &nbsp;</p>

可能的解决方案:在每次迭代中检查节点是否存在。

Also it's important not to increment i since the length of the array keeps shrinking, the first item gets removed on every iteration until no more items are left

var paragraphs = document.getElementsByTagName('p')

for (var i = 0, len = paragraphs.length; i < len; ) {
   if(  paragraphs[i] )
     paragraphs[i].remove()
}
<p>1</p>
<p>2</p>
<p>3</p>

可能的修复方法:我几乎总是更喜欢使用反向迭代器。

var paragraphs = document.getElementsByTagName('p')

for (var i = paragraphs.length; i--; ){
    paragraphs[i].remove() // could also use `paragraphs[0]`. "i" index isn't necessary
}
<p>1</p>
<p>2</p>
<p>3</p>


0
你必须在其父节点上使用removeChild来实现这一点:
for (var i = 0, len = paragraphs.length; i < len; i++) {
    paragraphs[i].parentNode.removeChild(paragraphs[i]);
}

编辑:从这里看起来,plnkr运行得不好,所以我没有测试过,但应该可以工作:

window.addEventListener("load", function() {
    var paragraphs = document.getElementsByTagName('p');

    var loop = function() {
        for (var i = 0, len = paragraphs.length; i < len; i++) {
            paragraphs[i].parentNode.removeChild(paragraphs[i]);
        }
    };

    console.log(paragraphs.length) // 7
    loop();
    console.log(paragraphs.length) // 3
    loop();
    console.log(paragraphs.length) // 1
    loop();
    console.log(paragraphs.length) // 0
});

抱歉,我忘了提到我也尝试过这个:http://plnkr.co/edit/Q8PnMQs8gsCXzo5mAUIl?p=preview - Marc Kline
@MarcKline 你正在直接将脚本运行到头部。这意味着它将在页面(及其内容)加载之前运行。你必须在“onload”事件中执行此操作。 - Sebastien C.
谢谢你的帮助,但这并没有起作用,最后一条评论也不正确。我在闭合body标签之前加载脚本。 - Marc Kline

-1
在我的情况下,这是我的解决方案:
var temp = document.getElementsByClassName('level-size');
for (var i = 0, len = temp.length; i < len; i++)
    temp[0].remove();

temp[0] 是因为每次我移除它时,整个表格都会向后推一个索引。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接