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

40

注意:在假设此问题是重复的之前,请查看本问题底部的一小节,解释为什么其他类似问题没有提供我所寻找的答案。


我们都知道将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;
}

这一方法一直很有效,但我发现对于某些元素(例如SVG),它却不起作用。尽管我只在Chrome中测试过,但它不会附加属性。


看起来这应该是一个容易解决的问题,但为什么我不能使用NodeList构造函数创建NodeList,并且为什么我不能像将NodeLists转换为数组那样以类似的方式将数组转换为NodeList?

如何正确地将节点数组转换为NodeList?


类似的问题有解决方案,但并不适用于我的问题:

以下问题与此问题相似。不幸的是,这些问题/答案并没有解决我的特定问题,原因如下。

如何将元素数组转换为NodeList? 这个问题中的答案使用了克隆节点的方法。这将不起作用,因为我需要访问原始节点。

在JavaScript中从单个节点创建节点列表使用了文档片段方法(第1次尝试)。其他答案在第2和第3次尝试中尝试类似的方法。

创建DOM NodeList正在使用E4X,因此不适用。即使它使用了这个方法,它仍然会从DOM中删除元素。


3
请勿忘失败总是一种选择。我个人更喜欢 querySelectorAll 返回一个数组。NodeList 唯一的优点在于它有可能是动态的,但实际意义不大。 - 4castle
@apsillers,是的,那比我尝试添加的属性要好。但你说的避免使用突变观察器是正确的。我有很多正在使用的 Web 组件依赖于 className 的更改等。 - KevBot
1
你的第一次尝试可能也不起作用,因为.childNodes是一个动态的NodeList,而不是静态的。 - Bergi
1
@Bergi,我都试过了。在文档片段的上下文中,:scope 是不起作用的。我还尝试将 find 原型方法添加到 DocumentFragment 中,但也没有起作用。我使用了 这个答案 中的一些 polyfill 想法。 - KevBot
1
@Teemu:我试了一下,虽然它完美地模拟了 NodeList,并且比我的第二次尝试要好得多,但最好还是把它作为 NodeList 的一个实例。不过,可以通过 Array.from(...) 来调用它也很不错。 - KevBot
显示剩余8条评论
5个回答

20

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

因为NodeList接口的DOM规范没有指定WebIDL [Constructor]属性,所以它无法直接在用户脚本中创建。

为什么我不能像将NodeLists转换为数组一样将数组强制转换为NodeList?

这确实是在你的情况下很有用的功能,但DOM规范并没有指定存在这样的函数。因此,不可能直接从Node数组中填充NodeList

虽然我严重怀疑您会称这为“正确的方法”,但一个丑陋的解决方案是找到唯一选择所需元素的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路径一般)无法选择文本节点。

我不知道这是否足够高效适合您的目的。


这就是我所说的寻找另一种方法的意思。 - Pablo Lozano
这绝对是一种有创意的方法!我喜欢它实际上创建了一个NodeList,并且不会影响我在Web组件上观察类属性的任何变异观察器。只是提醒一下,这偶尔会捕获错误的用例。这里有一个JS fiddle,展示了当我仅针对嵌套选择元素时,它也获取了传递给转换器的第一个选项元素。 - KevBot
1
@KevBot 哎呀,那是我的 CSS 错误;现在已经修复了。我需要使用 > 来明确表示每个 :nth-child 之间的父子关系,而不是一般的祖先后代关系,这由空格表示。(它出问题是因为 <option> 实际上匹配了最终的 :nth-child(1) 伪元素,并且它有祖先,在某个地方,与其他正确顺序的 :nth-child 匹配。没有 > 它对这些祖先可以存在的位置太宽容了。) - apsillers

4
这是我的建议:
  • Document 是一个原生对象,扩展它可能不是一个好主意。
  • NodeList 是一个具有私有构造函数和没有公共方法来添加元素的本地对象,肯定有它的原因。
  • 除非有人能提供一种方法,否则没有办法创建和填充 NodeList,而不修改当前文档。
  • NodeList 就像一个数组,但它具有 item 方法,该方法的工作方式就像使用方括号一样,唯一的例外是当您超出范围时返回 null 而不是 undefined。你可以返回一个实现了 item 方法的数组:

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

PS:也许你采取的方法是错误的,你的自定义查询方法只需要包装一个 document.querySelectorAll 调用即可返回你要查找的内容。


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;
    };
// Usage:
var nodeListFromArray = getNodeList(arrayOfNodes);

这个解决方案还存在一些问题。 instanceof 运算符无法将返回的对象识别为 NodeList。此外,控制台日志和 dirrings 的显示方式与 NodeList 不同。
(* = 一个 for 循环用于迭代传递的数组,以便该函数也可以接受传递的 NodeList。如果您更喜欢 forEach 循环,则也可以使用,只要仅传递数组即可。) jsFiddle 上的演示

一个本地的NodeList永远不是nullish的。也许最好返回列表,无论是空的还是非空的。 - Gust van de Wal
@GustvandeWal 这段代码总是返回一个列表。如果您尝试读取不存在的索引(就像 ES5 NodeList 一样),它会返回 null - Teemu
什么??我发表评论后30分钟你就已经解决了它。看看你的最后一次编辑。(我的手机上无法@你) - Gust van de Wal

2

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

1
这个实现不仅将原始元素放入新的NodeList中,还克隆了它们,这是OP明确不想要的。我也想知道这里会发生什么,比如colfigcaptionli元素;虽然没有测试过。最后,childNodes返回一个动态的NodeList,而不是静态的。尽管如此,我喜欢这种简单的方法 :) - Gust van de Wal

1

保罗·S在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)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接