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

javascript dom nodelist

2377 просмотра

4 ответа

10809 Репутация автора

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


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

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

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


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

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

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

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


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

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


Подобные вопросы, ответы на которые у меня не работают:

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

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

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

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

Автор: KevBot Источник Размещён: 18.07.2016 03:22

Ответы (4)


4 плюса

8149 Репутация автора

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

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

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

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

Автор: Pablo Lozano Размещён: 18.07.2016 04:19

19 плюса

89277 Репутация автора

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

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

почему я не могу привести массив к NodeList так же, как NodeLists - к массивам?

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

Хотя я серьезно сомневаюсь, что вы бы назвали это «правильным путем» для решения проблем, одно уродливое решение - найти 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 в целом) не могут выбирать текстовые узлы.

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

Автор: apsillers Размещён: 18.07.2016 04:39

4 плюса

18915 Репутация автора

Поскольку кажется, что создание реального 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. Кроме того, журналы консоли и директории отображаются иначе, чем NodeList.

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

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

Автор: Teemu-callmewhateveryouwant Размещён: 18.07.2016 09:33

1 плюс

1439 Репутация автора

Вы можете использовать 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;
}
Автор: Chayim Friedman Размещён: 27.02.2018 04:46
Вопросы из категории :
32x32