Как я могу преобразовать массив узлов в статический NodeList?

ПРИМЕЧАНИЕ. Прежде чем этот вопрос будет считаться повторяющимся, в нижней части этого вопроса есть раздел, в котором объясняется, почему несколько похожих вопросов не дают ответа, который я ищу.


Мы все знаем, что преобразовать NodeList в массив легко, и есть много способов сделать это:

[].slice.call(someNodeList)
// or
Array.from(someNodeList)
// etc...

Мне нужно обратное; как преобразовать массив узлов в статический список узлов?


Почему я хочу это сделать?

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

Document.prototype.customQueryMethod = function (...args) {...}

Пытаясь сохранить верность тому, как работает querySelectorAll, я хочу вернуть статическую коллекцию NodeList вместо массива.


До сих пор я подходил к проблеме тремя разными способами:

Попытка 1:

Создание фрагмента документа

function createNodeList(arrayOfNodes) {
    let fragment = document.createDocumentFragment();
    arrayOfNodes.forEach((node) => {
        fragment.appendChild(node);
    });
    return fragment.childNodes;
}

Хотя это возвращает NodeList, это не работает, потому что вызов appendChild удаляет узел из его текущего местоположения в DOM (где он должен оставаться).

Другой вариант этого включает cloning узлы и возврат клонов. Однако теперь вы возвращаете клонированные узлы, которые не имеют ссылок на фактические узлы в DOM.


Попытка 2:

Попытка «издеваться» над конструктором NodeList

const FakeNodeList = (() => {

    let fragment = document.createDocumentFragment();
    fragment.appendChild(document.createComment('create a nodelist'));

    function NodeList(nodes) {
        let scope = this;
        nodes.forEach((node, i) => {
            scope[i] = node;
        });
    }

    NodeList.prototype = ((proto) => {
        function F() {
        }

        F.prototype = proto;
        return new F();
    })(fragment.childNodes);

    NodeList.prototype.item = function item(idx) {
        return this[idx] || null;
    };

    return NodeList;
})();

И он будет использоваться следующим образом:

let nodeList = new FakeNodeList(nodes);

// The following tests/uses all work
nodeList instanceOf NodeList // true
nodeList[0] // would return an element
nodeList.item(0) // would return an element

Хотя этот конкретный подход не удаляет элементы из DOM, он вызывает другие ошибки, например, при преобразовании его в массив:

let arr = [].slice.call(nodeList);
// or
let arr = Array.from(nodeList);

Каждое из вышеперечисленных действий приводит к следующей ошибке: Uncaught TypeError: Illegal invocation

Я также пытаюсь избежать «имитации» списка узлов с помощью поддельного конструктора списка узлов, поскольку я считаю, что это, вероятно, будет иметь непредвиденные последствия в будущем.


Попытка 3:

Прикрепление временного атрибута к элементам для повторного запроса к ним

function createNodeList(arrayOfNodes) {
    arrayOfNodes.forEach((node) => {
        node.setAttribute('QUERYME', '');
    });
    let nodeList = document.querySelectorAll('[QUERYME]');
    arrayOfNodes.forEach((node) => {
        node.removeAttribute('QUERYME');
    });
    return nodeList;
}

Это работало хорошо, пока я не обнаружил, что это не работает для некоторых элементов, таких как SVG. Он не прикрепит атрибут (хотя я проверял это только в Chrome).


Кажется, это должно быть легко сделать, почему я не могу использовать конструктор NodeList для создания NodeList и почему я не могу преобразовать массив в NodeList аналогично тому, как NodeList преобразуются в массивы?

Как правильно преобразовать массив узлов в NodeList?


Похожие вопросы, ответы на которые меня не устраивают:

Следующие вопросы аналогичны этому. К сожалению, эти вопросы/ответы не решают мою конкретную проблему по следующим причинам.

Как я могу преобразовать массив элементов в NodeList? В ответе на этот вопрос используется метод клонирования узлов. Это не сработает, потому что мне нужен доступ к исходным узлам.

Создать список узлов из одного узла в JavaScript использует подход фрагмента документа (Попытка 1). Другие ответы пытаются сделать то же самое в попытках 2 и 3.

Создание DOM NodeList использует E4X и поэтому не применяется. И даже несмотря на то, что он использует это, он все равно удаляет элементы из DOM.


person KevBot    schedule 18.07.2016    source источник
comment
Не забывайте, что неудача всегда возможна. Лично я бы предпочел, чтобы querySelectorAll возвращал массив. Единственная реальная польза от NodeList — это возможность работать вживую.   -  person 4castle    schedule 18.07.2016
comment
Методы @Teemu, такие как document.getElementsbyTagName, возвращают NodeList, не удаляя элементы из документа. Это поведение, которое OP пытается имитировать   -  person Pablo Lozano    schedule 18.07.2016
comment
Если собственный NodeList использует appendChild, как NodeList сохраняет элементы там, где они были до запроса? Моя цель — сохранить узлы в том месте, где они были до запроса, например, как работает querySelectorAll и т. д. Если это имеет смысл. Комментарий @Pablo правильный.   -  person KevBot    schedule 18.07.2016
comment
Существует попытка, аналогичная вашей попытке № 2, которая добавляет классы вместо атрибутов: gist.github.com/marcoos /1143928 Но это по-прежнему не будет работать с текстовыми узлами (и вы, возможно, будете против добавления классов, например, если вы обеспокоены тем, что наблюдатели мутаций увидят изменение, или удаленная возможность столкновения с настоящее имя класса)   -  person apsillers    schedule 18.07.2016
comment
@apsillers, да, это сработает лучше, чем атрибут, который я пытался добавить. Но вы правы в том, что избегаете наблюдателей за мутациями. У меня есть ряд используемых веб-компонентов, которые полагаются на изменение className и т.д.   -  person KevBot    schedule 18.07.2016
comment
Ваша первая попытка, вероятно, также не сработает, потому что .childNodes — это живой NodeList, а не статический.   -  person Bergi    schedule 18.07.2016
comment
@ Берги, я не думал, что это может быть так. Мне нужно будет изучить, есть ли способ сделать его статическим из этого контекста. Хотя поначалу я отношусь к этому скептически, так как appendChild нужен в первую очередь в этой попытке.   -  person KevBot    schedule 18.07.2016
comment
@KevBot: Можешь попробовать fragment.querySelectorAll(":scope > *") или fragment.find("> *")? Это должно дать вам статический список узлов, и вы можете переместить узлы в их предыдущее местоположение в DOM после их выбора.   -  person Bergi    schedule 18.07.2016
comment
@ Берги, я пробовал оба. :scope не работает в контексте фрагмента документа. Я также пытался добавить прототипный метод find к DocumentFragment, который также не работал. Я использовал некоторые идеи полифилла из этот ответ   -  person KevBot    schedule 18.07.2016
comment
Как насчет создания обычного объекта JS, который эмулирует NodeList?   -  person Teemu    schedule 18.07.2016
comment
@Teemu, это похоже на мою вторую попытку. Вы имеете в виду что-то другое?   -  person KevBot    schedule 18.07.2016
comment
Да, обычный JS-объект с самодельным прототипом. Что-то вроде этого.   -  person Teemu    schedule 18.07.2016
comment
@Teemu: я попробовал. Хотя это прекрасно имитирует NodeList и НАМНОГО лучше, чем моя вторая попытка, в идеале это также был бы экземпляр NodeList. Хотя приятно, что я могу вызвать для него Array.from(...) .   -  person KevBot    schedule 18.07.2016


Ответы (5)


почему я не могу использовать конструктор NodeList для создания NodeList

Поскольку спецификация DOM для интерфейса NodeList не указывает атрибут WebIDL [Конструктор], поэтому его нельзя создать непосредственно в пользовательских скриптах.

почему я не могу преобразовать массив в NodeList так же, как NodeList преобразуются в массивы?

Это, безусловно, было бы полезно иметь в вашем случае, но такая функция не указана в спецификации DOM. Таким образом, невозможно напрямую заполнить NodeList из массива Node.

Хотя я серьезно сомневаюсь, что вы назовете это «правильным способом», одно уродливое решение — найти селекторы CSS, которые однозначно выбирают нужные вам элементы, и передать все эти пути в querySelectorAll в качестве селектора, разделенного запятыми:

// find a CSS path that uniquely selects this element
function buildIndexCSSPath(elem) {
    var parent = elem.parentNode;

     // if this is the root node, include its tag name the start of the string
    if(parent == document) { return elem.tagName; } 

    // find this element's index as a child, and recursively ascend 
    return buildIndexCSSPath(parent) + " > :nth-child(" + (Array.prototype.indexOf.call(parent.children, elem)+1) + ")";
}

function toNodeList(list) {
    // map all elements to CSS paths
    var names = list.map(function(elem) { return buildIndexCSSPath(elem); });

    // join all paths by commas
    var superSelector = names.join(",");

    // query with comma-joined mega-selector
    return document.querySelectorAll(superSelector);
}

toNodeList([elem1, elem2, ...]);

Это работает путем поиска строк CSS для уникального выбора каждого элемента, где каждый селектор имеет форму html > :nth-child(x) > :nth-child(y) > :nth-child(z) .... То есть можно понимать, что каждый элемент существует как дочерний элемент дочернего элемента дочернего элемента (и т. д.) вплоть до корневого элемента. Найдя индекс каждого потомка в пути предка узла, мы можем однозначно идентифицировать его.

Обратите внимание, что это не сохранит узлы типа Text, потому что querySelectorAll (и пути CSS в целом) не могут выбирать текстовые узлы.

Я понятия не имею, будет ли это достаточно производительным для ваших целей.

person apsillers    schedule 18.07.2016
comment
Вот что я имел в виду под поиском другого подхода - person Pablo Lozano; 19.07.2016
comment
Это определенно творческий подход! Мне нравится, что он на самом деле создает NodeList и не влияет ни на одного из наблюдателей мутаций, которые я наблюдаю за атрибутами класса в своих веб-компонентах. В качестве предупреждения, это время от времени фиксирует неправильный вариант использования. Вот скрипт JS, который показывает, как, когда я нацеливаюсь только на вложенный элемент select, он также получает первый option элемент после передачи его конвертеру. - person KevBot; 19.07.2016
comment
@KevBot К сожалению, это моя ошибка CSS; исправлено сейчас. Мне нужно использовать >, чтобы конкретно указать отношение родитель-потомок между каждым :nth-child вместо общего отношения предок-потомок, которое обозначается пробелом. (Это ломалось, потому что <option> соответствовал последнему псевдоэлементу :nth-child(1), и у него где-то были предки, которые соответствовали другим :nth-child в правильном порядке. Без > было слишком снисходительно относиться к тому, где эти предки может быть.) - person apsillers; 19.07.2016

Вот мои два цента:

  • Document — это нативный объект, и его расширение может быть плохой идеей.
  • NodeList — это собственный объект с частным конструктором и без общедоступных методов для добавления элементов, и для этого должна быть причина.
  • Если кто-то не может предоставить хак, невозможно создать и заполнить NodeList без изменения текущего документа.
  • NodeList подобен массиву, но имеет метод item, который работает так же, как использование квадратных скобок, за исключением возврата null вместо undefined, когда вы находитесь вне диапазона. Вы можете просто вернуть массив с реализованным методом элемента:

myArray.item= function (e) { return this[e] || null; }

PS: Возможно, вы используете неправильный подход, и ваш собственный метод запроса может просто обернуть вызов document.querySelectorAll, который возвращает то, что вы ищете.

person Pablo Lozano    schedule 18.07.2016

Поскольку кажется, что создание реального NodeList из массива имеет серьезные резервы, возможно, вместо этого вы могли бы использовать обычный объект JS с самодельным прототипом для эмуляции NodeList. Вот так:

var nodeListProto = Object.create({}, {
        item: {
            value: function(x) {
                return (Object.getOwnPropertyNames(this).indexOf(x.toString()) > -1) ? this[x] : null;
            },
            enumerable: true
        },
        length: {
            get: function() {
                return Object.getOwnPropertyNames(this).length;
            },
            enumerable: true
        }
    }),
    getNodeList = function(nodes) {
        var n, eN = nodes.length,
            list = Object.create(nodeListProto);
        for (n = 0; n < eN; n++) { // *
            Object.defineProperty(list, n.toString(), {
                value: nodes[n],
                enumerable: true
            });
        }
        return list;
    };
// Usage:
var nodeListFromArray = getNodeList(arrayOfNodes);

Есть еще некоторые запасные варианты с этим решением. Оператор instanceof не может распознать возвращаемый объект как NodeList. Кроме того, консольные журналы и каталоги отображаются иначе, чем NodeList.

(* = Цикл for используется для итерации переданного массива, чтобы функция могла принять и переданный NodeList. Если вы предпочитаете цикл forEach, его также можно использовать, если будет передан только массив.)

Прямая демонстрация на jsFiddle.

person Teemu    schedule 18.07.2016
comment
Собственный NodeList никогда не бывает нулевым. Может быть, лучше вернуть список, пустой или нет - person Gust van de Wal; 20.09.2020
comment
@GustvandeWal Код всегда возвращает список. Он возвращает null, если вы пытаетесь прочитать несуществующий индекс (как это делал ES5 NodeList). - person Teemu; 16.04.2021
comment
Что?? Вы уже разрешили мой комментарий через 30 минут после того, как я его разместил. См. ваше последнее редактирование. (Это не позволит мне @тебя на моем телефоне) - person Gust van de Wal; 17.04.2021

Вы можете использовать свойство outerHTML каждого элемента и добавить его к родительскому элементу (который будет создан с помощью document.createElement(), тип элемента не имеет значения). Например, в ES6:

function getNodeList(elements) {
  const parentElement = document.createElement('div');
  // This can be a differnet element type, too (but only block (display: block;) element, because it impossible to put block element in inline element, and maybe 'elements' array contains a block element).
  let HTMLString = '';
  for (let element of elements) {
    HTMLString += element.outerHTML;
  }

  parentElement.innerHTML = HTMLString;

  return parentElement.childNodes;
}
person Chayim Friedman    schedule 27.02.2018
comment
Вместо того, чтобы помещать исходные элементы в новый NodeList, эта реализация также клонирует их, чего явно не хочет OP. Мне также интересно, что происходит, скажем, с элементами col, figcaption или li здесь; правда ничего не проверял. Наконец, childNodes возвращает живой NodeList, а не статический. Все это говорит о том, что я люблю простоту :) - person Gust van de Wal; 20.09.2020

Пол С. написал ответ в 2013 году, на котором он основан.

var toNodeList = (function() {      
  // Create a document fragment
  var emptyNL = document.createDocumentFragment().childNodes;

  // This is returned from a self-executing function so that
  // the DocumentFragment isn't repeatedly created.
  return function(nodeArray) {
    // Check if it's already a nodelist.
    if (nodeArray instanceof NodeList) return nodeArray;

    // If it's a single element, wrap it in a classic array.
    if (!Array.isArray(nodeArray)) nodeArray = [nodeArray];

    // Base an object on emptyNL
    var mockNL = Object.create(emptyNL, {
      'length': {
        value: nodeArray.length, enumerable: false
      },
      'item': {
        "value": function(i) {
          return this[+i || 0];
        },
        enumerable: false
      }
    });

    // Copy the array elemnts
    nodeArray.forEach((v, i) => mockNL[i] = v);

    // Return an object pretending to be a NodeList.
    return mockNL;
  }
}())
    
    
var arr = document.querySelectorAll('body');
console.log('Actual NodeList is NodeList?', arr instanceof NodeList)
arr = Array.from(arr)
console.log('Plain Array is NodeList?', arr instanceof NodeList)
arr = toNodeList(arr)
console.log('Emulated NodeList is NodeList?', arr instanceof NodeList)

person Regular Jo    schedule 10.06.2021