如何使用纯JavaScript获取给定元素的所有父节点?

40

我的意思是它们的数组。也就是从顶层HTML到目标元素的链,包括元素本身。

例如对于元素<A>,它将是:

[HTML, BODY, DIV, DIV, P, SPAN, A]
7个回答

64

稍微简短一些(也更加安全,因为target可能不存在):

var a = document.getElementById("target");
var els = [];
while (a) {
    els.unshift(a);
    a = a.parentNode;
}

9
我更改了被接受的答案,这是最优雅的。(顺便说一下,测试表明先推再反转更有效率)。[我希望有一个选项可以接受多个答案,即使需要花费我的分数也可以。] - rsk82
3
这将包括文档节点。如果您希望停留在 HTML 元素处,则将语句 'a = a.parentNode' 改为 a = a.parentElement - Jack G
2
更加优雅,只是为了好玩:const parents = node => (node.parentElement ? parents(node.parentElement) : []).concat([node]); - Nate Abele
1
@rsk82 九年半后,push+reverse仍然快了400%。哇。 - Regular Jo
这里没有理由使用 unshift。它的时间复杂度是二次方的。 - ggorlen
显示剩余3条评论

26

你可以尝试类似这样的代码:

var nodes = [];
var element = document.getElementById('yourelement');
nodes.push(element);
while(element.parentNode) {
    nodes.unshift(element.parentNode);
    element = element.parentNode;
}

4
+1. 我也想这么说,但是在结尾不要使用 reverse(),而是应该使用 unshift() 替代 push() - J. Holmes
1
+1 针对该信息。我认为相比于 push 和 reverse,unshift 使代码看起来更加清晰。如果我们不处理数百或数千个数组项,性能应该不会有明显的影响。 - techfoobar
2
这整个时间我从未知道.push()还有一个叫做.unshift()的兄弟! - Robin Maben
2
如果我们都非常担心速度问题,为什么不改成 var nodes = [element]; 同时交换前两行,并在循环之前删除 push。我认为先将所有元素 push 进数组然后再 reverse 更快。重复的 unshift 会是 O(n^2),因为每次迭代都要移动越来越多的元素。push 然后 reverse 可以利用 push 优化的数组和在原地修改的 reverse。在可合理实现的情况下,推进(Push)很可能是摊销的常数时间。reverse 是 O(n),在原地操作中非常快。 - doug65536
如果节点不存在,element.parentNode 将会抛出异常。而且没有理由为了节省一行代码而推荐使用 unshift,因为这将导致复杂性大大增加。 - ggorlen
显示剩余4条评论

7
我喜欢这种方法:
[...(function*(e){do { yield e; } while (e = e.parentNode);})($0)]

...其中$0是你的元素。

这种方法的优点是它可以作为表达式中的值使用。

要获取没有目标元素的数组:

[...(function*(e){while (e = e.parentNode) { yield e; }})($0)]

目前最短的解决方案。如果您想删除目标元素本身,可以在末尾添加.slice(1),如果您想先得到最大的父级,请添加.reverse() - undefined
3
为了排除目标元素,更容易将 do-while 循环反转为 while 循环,像这样:[...(function*(e){while (e = e.parentNode) { yield e; }})($0)](我已更新我的答案) - Yuriy Ershov

5

您可以遍历 element.parentNode 的链,直到到达 falsey 值,在此过程中将内容附加到数组中:

const getParents = el => {
  for (var parents = []; el; el = el.parentNode) {
    parents.push(el);
  }

  return parents;
};

const el = document.querySelector("b");
console.log(getParents(el).reverse().map(e => e.nodeName));
<div><p><span><b>Foo</b></span></div>

请注意,反转是在调用者中完成的,因为它对于继承算法并不重要。映射到e.nodeName仅用于演示,也不是必要的。
请注意,这种方法意味着您将以文档元素作为链中的最后一个元素结束。如果您不想这样,可以将&& el !== document添加到循环停止条件中。
上述代码的总时间复杂度是线性的,并且reverse()是原地操作,因此不需要额外的分配。在循环中使用unshift,正如其他答案建议的那样,是二次的,并且可能会在深度不常见的DOM树上损害可扩展性,而换来的优雅程度微不足道。

2
另一种选择(基于这个):链接
for(var e = document.getElementById("target"),p = [];e && e !== document;e = e.parentNode)
p.push(e);

0

我相信,如果您经常使用此函数,这将是最具性能的方案从长远来看在大多数情况下。之所以如此,是因为它最初会检查可能遇到的祖先深度。此外,该函数不会在每次调用时创建新数组,而是会高效地重复使用同一数组,并对其进行切片,在某些浏览器中非常优化。但是,由于我不知道任何真正有效的方法来检查最大深度,因此我只能使用效率较低的查询选择器检查。

// !IMPORTANT! When moving this coding snippet over to your production code,
// do not run the following depthtest more than once, it is not very performant
var kCurSelector="*|*", curDepth=3;
while (document.body.querySelector(kCurSelector += '>*|*')) curDepth++;
curDepth = Math.pow(2, Math.ceil(Math.log2(startDepth))),
var parentsTMP = new Array(curDepth);

function getAllParentNodes(Ele){
    var curPos = curDepth;

    if (Ele instanceof Node)
      while (Ele !== document){
        if (curPos === 0){
          curPos += curDepth;
          parentsTMP.length <<= 1;
          parentsTMP.copyWithin(curDepth, 0, curDepth);
          curDepth <<= 1;
        }
        parentsTMP[--curPos] = Ele;
        Ele = Ele.parentNode;
      }
    return retArray.slice(curPos)
}

上述函数的浏览器兼容性是它可以在Edge中工作,但不能在IE中工作。如果您需要IE支持,则需要一个Array.prototype.copyWithin填充程序。


-2

在 JavaScript 数组中获取子节点的所有父节点

let selectedTxtElement = document.getElementById("target");
    let els = [];
    while (selectedTxtElement) {
         els.unshift(selectedTxtElement);
         selectedTxtElement = selectedTxtElement.parentNode;
    }

了解更多


1
除了变量名之外,这与十年前发布的被接受答案完全相同。该链接指向一个非法的 Stack Overflow 爬取/冒名顶替网站,窃取了此线程的被接受答案...我在这里看不到任何价值。 - ggorlen

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