Подтвердить что ты не робот

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

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


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

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

То, чем я являюсь, - это обратное; Как преобразовать массив узлов в статический NodeList?


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

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

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 удаляет node из его текущего местоположения в 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

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


Попытка 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 так же, как NodeLists, которые будут переданы в массивы

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


Аналогичные вопросы, которые не отвечают на меня:

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

В то время как я серьезно сомневаюсь, что вы бы назвали это "правильным способом", чтобы разобраться в вещах, одно уродливое решение - это поиск селекторов 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 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) .... То есть каждый элемент может быть понят, что он существует как дочерний элемент дочернего элемента ребенка (и т.д.), Вплоть до корневого элемента. Найдя индекс каждого дочернего элемента в пути node предка, мы можем его однозначно идентифицировать.

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

Я понятия не имею, достаточно ли это для ваших целей.

Ответ 2

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

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

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

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

Ответ 3

Так как кажется, что создание реального 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.length) ? list : null;
    };
// Usage:
var nodeListFromArray = getNodeList(arrayOfNodes);

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

Цикл

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

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