如何将节点数组转换为静态NodeList?

时间:2016-07-18 15:22:13

标签: javascript dom nodelist

注意:在假设此问题重复之前,此问题底部有一个部分解决了为什么一些类似的问题无法提供我正在寻找的答案。

我们都知道将NodeList转换为数组很容易,有很多方法可以做到:

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

我所追求的是相反的; 如何将节点数组转换为静态NodeList?

为什么我要这样做?

我没有深入研究,我正在创建一种新方法来查询页面上的元素,即:

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

为了忠于querySelectorAll的工作方式,我想返回static collection 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

我也在努力避免"模仿"一个带有伪节点列表构造函数的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&#39> 。它不会附加属性(虽然我只在Chrome中测试过这个)。

这似乎应该是一件容易的事情,为什么我不能使用NodeList构造函数来创建NodeList,为什么不能以与NodeLists类似的方式将数组转换为NodeList被转换为数组?

如何以正确的方式将节点数组转换为NodeList?

有类似问题的答案对我不起作用:

以下问题与此类似。不幸的是,由于以下原因,这些问题/答案无法解决我的特定问题。

How can I convert an Array of elements into a NodeList?此问题的答案使用克隆节点的方法。这不起作用,因为我需要访问原始节点。

Create node list from a single node in JavaScript使用文档片段方法(尝试1)。其他答案在Attempts 2和3尝试类似的事情。

Creating a DOM NodeList正在使用E4X,因此不适用。即使它正在使用它,它仍然从DOM中删除元素。

5 个答案:

答案 0 :(得分:19)

  

为什么我不能使用NodeList构造函数来创建NodeList

由于DOM specification for the NodeList interface未指定the WebIDL [Constructor] attribute,因此无法直接在用户脚本中创建。

  

为什么我不能以类似于NodeLists转换为数组的方式将数组转换为NodeList?

在你的情况下,这肯定是一个有用的函数,但是在DOM规范中没有指定这样的函数。因此,无法直接从NodeList s数组中填充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路径)无法选择文本节点。

我不知道这是否足以满足您的目的。

答案 1 :(得分:4)

这是我的两分钱:

  • 文档是一个本机对象,扩展它可能不是一个好主意。
  • NodeList是一个带有私有构造函数的本机对象,没有公共方法来添加元素,并且必须有原因。
  • 除非有人能够提供黑客攻击,否则无法在不修改当前文档的情况下创建和填充NodeList。
  • NodeList就像一个数组,但item方法就像使用方括号一样,但当你超出范围时返回null而不是undefined除外。您只需返回一个实现了item方法的数组:

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

PS:也许你采取了错误的方法,你的自定义查询方法可能只包含一个document.querySelectorAll调用,返回你想要的内容。

答案 2 :(得分:4)

由于似乎从数组创建一个真正的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不同。

(* = A for循环用于迭代传递的数组,因此该函数也可以接受传递的NodeList。如果您更喜欢forEach循环,也可以使用它,只要数组只传递。)

A live demo at jsFiddle

答案 3 :(得分:1)

您可以使用每个元素的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;
}

答案 4 :(得分:0)

Paul S. wrote an answer in 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)