Удаление HTMLCollection элементов из DOM

У меня есть коллекция элементов абзаца. Некоторые пустые, а некоторые содержат только пробелы, в то время как другие имеют контент:

<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();
}

Нет ошибки, но не все элементы удалены. Поэтому я запускаю его снова, и он удаляет некоторые элементы, которые не были удалены ранее. Я запускаю его снова и, наконец, все абзацы удаляются из документа.

Мне интересно, какие очевидные детали мне здесь не хватает.

Демо проблемы

4 ответа

Решение

Проблема в том, что paragraphs это живой список. Удалив 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 не удаляется, поскольку пропускается.

Длина вашей коллекции HTMLC изменяется при удалении элемента. Один из способов сделать это - использовать цикл while

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

Почему у вас это не работает:

HTMLCollection мутирует (изменение) в то время как вы удаляете узлы, то длина получает вне синхронизации с "реальной" длиной HTMLCollection массива.

Допустим, у вас есть массив из 2 узлов DOM, и вы его повторяете. он должен повторяться 2 раза. Демо ниже прекрасно иллюстрирует это, и мне легко следовать:

первая итерация - удаляет первый узел, а затем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>

Возможное исправление: на каждой итерации проверка существует узел

Также важно не увеличиватьi поскольку длина массива продолжает сокращаться, первый элемент удаляется на каждой итерации, пока не останется больше элементов

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>

Вы должны использовать 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
});

В моем случае это было мое решение:

var temp = document.getElementsByClassName('level-size');
for (var i = 0, len = temp.length; i < len; i++)
    temp[0].remove();

temp [0], потому что каждый раз, когда я его удаляю, таблица отверстий отодвигается на один индекс назад.

Другие вопросы по тегам