【问题标题】:How to find the nearest common ancestors of two or more nodes?如何找到两个或多个节点的最近共同祖先?
【发布时间】:2010-10-18 15:50:01
【问题描述】:

用户在 HTML 页面中选择了两个或多个元素。我想要完成的是找到这些元素的共同祖先(所以如果之前没有找到身体节点将是共同祖先)?

P.S:这可以通过 XPath 实现,但对我来说不是一个更好的选择。也可以通过 css 选择器解析找到它,但我认为这是一个肮脏的方法(?)

谢谢。

【问题讨论】:

  • 您想要“最近的”共同祖先,还是包含所有共同祖先的数组?
  • 只有“最近的”共同祖先
  • “XPath 可以实现,但对我来说不是一个更好的选择”——XPath 解决方案是什么?为什么它不适合你?

标签: javascript jquery


【解决方案1】:

这是一个更高效的纯 JavaScript 版本。

function parents(node) {
  var nodes = [node]
  for (; node; node = node.parentNode) {
    nodes.unshift(node)
  }
  return nodes
}

function commonAncestor(node1, node2) {
  var parents1 = parents(node1)
  var parents2 = parents(node2)

  if (parents1[0] != parents2[0]) throw "No common ancestor!"

  for (var i = 0; i < parents1.length; i++) {
    if (parents1[i] != parents2[i]) return parents1[i - 1]
  }
}

【讨论】:

  • 如果 DOM 节点是兄弟节点,这将失败。你应该编辑你的代码来解决这个问题。
  • (我尝试修复它,但我的更改正在等待批准,因为我认为这不是社区 wiki)
  • 您似乎根本不介意它的用途,但为了安全起见...我可以将它用作 MIT 许可的开源库的一部分吗?
  • @JacqueGoupil - 不确定它是否仍然重要,但正如网站页脚所说 - user contributions licensed under cc by-sa 3.0 with attribution required.
  • 由于unshift 是二次方,这并不像宣传的那样有效。 push/reverse 是线性的。
【解决方案2】:

涉及手动遍历祖先元素的解决方案远比必要的复杂。您不需要手动执行循环。用parents()获取一个元素的所有祖先元素,用has()将其缩减为包含第二个元素的元素,然后用first()获取第一个祖先。

var a = $('#a'),
    b = $('#b'),
    closestCommonAncestor = a.parents().has(b).first();

jsFiddle example

【讨论】:

  • +1 和 I stole your test case(希望你不介意);-)
  • +1 比我的解决方案好得多,尽管您有一年的时间来考虑它;-)
【解决方案3】:

这是另一个方法,它使用element.compareDocumentPosition()element.contains(),前者是标准方法,后者是除Firefox之外的大多数主要浏览器支持的方法:

比较两个节点

function getCommonAncestor(node1, node2) {
    var method = "contains" in node1 ? "contains" : "compareDocumentPosition",
        test   = method === "contains" ? 1 : 0x10;

    while (node1 = node1.parentNode) {
        if ((node1[method](node2) & test) === test)
            return node1;
    }

    return null;
}

工作演示:http://jsfiddle.net/3FaRr/(使用 lonesomeday 的测试用例)

这应该或多或少尽可能高效,因为它是纯 DOM 并且只有一个循环。


比较两个或更多节点

再看问题,我注意到“两个或更多” 要求的“或更多” 部分已被答案忽略。所以我决定稍微调整一下,以允许指定任意数量的节点:

function getCommonAncestor(node1 /*, node2, node3, ... nodeN */) {
    if (arguments.length < 2)
        throw new Error("getCommonAncestor: not enough parameters");

    var i,
        method = "contains" in node1 ? "contains" : "compareDocumentPosition",
        test   = method === "contains" ? 1 : 0x0010,
        nodes  = [].slice.call(arguments, 1);

    rocking:
    while (node1 = node1.parentNode) {
        i = nodes.length;    
        while (i--) {
            if ((node1[method](nodes[i]) & test) !== test)
                continue rocking;
        }
        return node1;
    }

    return null;
}

工作演示:http://jsfiddle.net/AndyE/3FaRr/1

【讨论】:

  • +1 由于its quirky behaviourcompareDocumentPosition 测试从未成功,我已经更正了示例。
  • @lonesomeday: 干杯,我忘了compareDocumentPosition 返回了一个位掩码。在 Firefox 中测试后,我再次更新了答案。
  • @AndyE a) 它可以在 npm 上吗? b) 你必须使用标签吗? >_
  • @Raynos: 干得好 :-) 我使用了一个标签,因为我更喜欢将它保存在一个函数中;我专注于效率。甚至 Crockford 的法律也允许使用标签,这说明了很多,因为它们不允许使用太多其他内容。它可能在 npm 上,但我写的大多数答案都是为 Stack Overflow 做的,并且通常不会花额外的时间将其提交到其他地方。
  • 整洁!相同性能代码的版本,也将其包装到 jQuery.fn.commonAncestor 插件中:jsfiddle.net/ecmanaut/Dv5Wj
【解决方案4】:

试试这个:

function get_common_ancestor(a, b)
{
    $parentsa = $(a).parents();
    $parentsb = $(b).parents();

    var found = null;

    $parentsa.each(function() {
        var thisa = this;

        $parentsb.each(function() {
            if (thisa == this)
            {
                found = this;
                return false;
            }
        });

        if (found) return false;
    });

    return found;
}

像这样使用它:

var el = get_common_ancestor("#id_of_one_element", "#id_of_another_element");

这很快就解决了,但它应该可以工作。如果你想要稍微不同的东西应该很容易修改(例如,返回 jQuery 对象而不是 DOM 元素,DOM 元素作为参数而不是 ID 等)

【讨论】:

    【解决方案5】:

    上面提到的 Range API 的 commonAncestorContainer 属性,连同它的 selectNode,让这很容易。

    在 Firefox 的 Scratchpad 或类似的编辑器中运行(“显示”)这段代码:

    var range = document.createRange();
    var nodes = [document.head, document.body];  // or any other set of nodes
    nodes.forEach(range.selectNode, range);
    range.commonAncestorContainer;
    

    请注意,IE 8 或更低版本不支持这两种 API。

    【讨论】:

    • 这不起作用,因为selectNode 将范围的开始和结束设置为引用节点的父节点,请参阅jsfiddle.net/3FaRr/13(应该提醒“thisOne”,但它没有找到正确的共同祖先)。
    【解决方案6】:

    您应该能够使用 jQuery .parents() 函数,然后遍历结果以查找第一个匹配项。 (或者我猜你可以从头开始,然后倒退,直到看到第一个区别;这可能会更好。)

    【讨论】:

    • 为了比较每个元素,我建议您制作一个单独的函数。这并不容易。简单的方法(不太好)是比较每个元素的innerhtml。最难(但非常好)是开始比较 id。如果它们都未定义,则比较类,如果两个元素具有相同的类(注意类的顺序),则可以比较正文中的元素索引。
    • @Diego:我相信两个 DOM 元素之间的直接比较应该是你所需要的,我相信
    • @Diego 因为实际上在 DOM 中的任何 DOM 元素(对于 DOM 中元素的所有父元素都是如此)只存在一次,即使是两个单独的 jQuery 数组也会引用完全相同的对象。因此,一个简单的=== 比较应该可以正常工作。
    【解决方案7】:

    您也可以使用 DOM Range(当然,当浏览器支持时)。如果您创建一个Range,其中startContainer 设置为文档中较早的节点,endContainer 设置为文档中的较晚节点,则此类RangecommonAncestorContainer attribute 是最深的共同祖先节点。

    下面是一些实现这个想法的代码:

    function getCommonAncestor(node1, node2) {
        var dp = node1.compareDocumentPosition(node2);
        // If the bitmask includes the DOCUMENT_POSITION_DISCONNECTED bit, 0x1, or the
        // DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC bit, 0x20, then the order is implementation
        // specific.
        if (dp & (0x1 | 0x20)) {
            if (node1 === node2) return node1;
    
            var node1AndAncestors = [node1];
            while ((node1 = node1.parentNode) != null) {
                node1AndAncestors.push(node1);
            }
            var node2AndAncestors = [node2];
            while ((node2 = node2.parentNode) != null) {
                node2AndAncestors.push(node2);
            }
    
            var len1 = node1AndAncestors.length;
            var len2 = node2AndAncestors.length;
    
            // If the last element of the two arrays is not the same, then `node1' and `node2' do
            // not share a common ancestor.
            if (node1AndAncestors[len1 - 1] !== node2AndAncestors[len2 - 1]) {
                return null;
            }
    
            var i = 1;
            for (;;) {
                if (node1AndAncestors[len1 - 1 - i] !== node2AndAncestors[len2 - 1 - i]) {
                    // assert node1AndAncestors[len1 - 1 - i - 1] === node2AndAncestors[len2 - 1 - i - 1];
                    return node1AndAncestors[len1 - 1 - i - 1];
                }
                ++i;
            }
            // assert false;
            throw "Shouldn't reach here!";
        }
    
        // "If the two nodes being compared are the same node, then no flags are set on the return."
        // http://www.w3.org/TR/DOM-Level-3-Core/core.html#DocumentPosition
        if (dp == 0) {
            // assert node1 === node2;
            return node1;
    
        } else if (dp & 0x8) {
            // assert node2.contains(node1);
            return node2;
    
        } else if (dp & 0x10) {
            // assert node1.contains(node2);
            return node1;
        }
    
        // In this case, `node2' precedes `node1'. Swap `node1' and `node2' so that `node1' precedes
        // `node2'.
        if (dp & 0x2) {
            var tmp = node1;
            node1 = node2;
            node2 = tmp;
        } else {
            // assert dp & 0x4;
        }
    
        var range = node1.ownerDocument.createRange();
        range.setStart(node1, 0);
        range.setEnd(node2, 0);
        return range.commonAncestorContainer;
    }
    

    【讨论】:

      【解决方案8】:

      这不再需要太多代码来解决:

      步骤:

      1. 获取 node_a 的父节点
      2. 如果 node_a 的 parent 包含 node_b,则返回 parent(在我们的代码中,parent 被称为 node_a)
      3. 如果 parent 不包含 node_b,我们需要继续上链
      4. 结束返回空

      代码:

      function getLowestCommonParent(node_a, node_b) {
          while (node_a = node_a.parentElement) {
              if (node_a.contains(node_b)) {
                  return node_a;
              }
          }
      
          return null;
      }
      

      【讨论】:

        【解决方案9】:

        基于Andy EAntonB 的回答

        处理边缘情况:node1 == node2node1.contains(node2)

        function getCommonParentNode(node1, node2) {
          if (node1 == node2) return node1;
          var parent = node1;
          do if (parent.contains(node2)) return parent
          while (parent = parent.parentNode);
          return null;
        }
        

        【讨论】:

        • 对我来说似乎很好(至少在查看元素时 - 不确定如何测试 TEXT 节点)。 jsfiddle.net/Abeeee/tjg6r1qw/11
        • 文本节点也有node.parentNode
        【解决方案10】:

        这是对 lonesomeday 答案的概括性看法。它将采用一个完整的 JQuery 对象,而不是只有两个元素。

        function CommonAncestor(jq) {
            var prnt = $(jq[0]);
            jq.each(function () { 
                prnt = prnt.parents().add(prnt).has(this).last(); 
            });
            return prnt;
        }
        

        【讨论】:

          【解决方案11】:

          聚会有点晚了,但这是一个优雅的 jQuery 解决方案(因为问题被标记为 jQuery)-

          /**
           * Get all parents of an element including itself
           * @returns {jQuerySelector}
           */
          $.fn.family = function() {
              var i, el, nodes = $();
          
              for (i = 0; i < this.length; i++) {
                  for (el = this[i]; el !== document; el = el.parentNode) {
                      nodes.push(el);
                  }
              }
              return nodes;
          };
          
          /**
           * Find the common ancestors in or against a selector
           * @param selector {?(String|jQuerySelector|Element)}
           * @returns {jQuerySelector}
           */
          $.fn.common = function(selector) {
              if (selector && this.is(selector)) {
                  return this;
              }
              var i,
                      $parents = (selector ? this : this.eq(0)).family(),
                      $targets = selector ? $(selector) : this.slice(1);
              for (i = 0; i < $targets.length; i++) {
                  $parents = $parents.has($targets.eq(i).family());
              }
              return $parents;
          };
          
          /**
           * Find the first common ancestor in or against a selector
           * @param selector {?(String|jQuerySelector|Element)}
           * @returns {jQuerySelector}
           */
          $.fn.commonFirst = function(selector) {
              return this.common(selector).first();
          };
          

          【讨论】:

          • @TrueBlueAussie 从那以后我实际上改进了我的代码,所以在这里更新 - 您的示例最大的问题是 .id 应该是 .attr("id")[0].id (因为它是 jQuery ) :-)
          • 酷。我在这里使用了另一个答案中的那个例子。应该发现这个错误。值得针对最简单的 a.parents().has(b).first() 答案进行快速测试。
          • .parents() 的问题在于它排除了自己(所以你最后需要 .addBack() ) - 这比我现在使用 .family() 手动创建它更昂贵- 可能值得针对 .family() 测试 .parents().addBack() :-P
          【解决方案12】:

          有点晚了,这里有一个 JavaScript ES6 版本,它使用 Array.prototype.reduce()Node.contains(),并且可以将任意数量的元素作为参数:

          function closestCommonAncestor(...elements) {
              const reducer = (prev, current) => current.parentElement.contains(prev) ? current.parentElement : prev;
              return elements.reduce(reducer, elements[0]);
          }
          
          const element1 = document.getElementById('element1');
          const element2 = document.getElementById('element2');
          const commonAncestor = closestCommonAncestor(element1, element2);
          

          【讨论】:

          • 看起来很棒,但似乎不起作用。试试&lt;div class="test1"&gt;&lt;div class="test"&gt;&lt;p&gt;Foo&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;Baz&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;。两个p 元素的正确共同祖先是div.test1,但此函数返回div.test
          【解决方案13】:

          这是一种更脏的方法。比较容易理解但是需要修改dom:

          function commonAncestor(node1,node2){
                  var tmp1 = node1,tmp2 = node2;
                  // find node1's first parent whose nodeType == 1
                  while(tmp1.nodeType != 1){
                      tmp1 = tmp1.parentNode;
                  }
                  // insert an invisible span contains a strange character that no one 
                  // would use
                  // if you need to use this function many times,create the span outside
                  // so you can use it without creating every time
                  var span = document.createElement('span')
                      , strange_char = '\uee99';
                  span.style.display='none';
                  span.innerHTML = strange_char;
                  tmp1.appendChild(span);
                  // find node2's first parent which contains that odd character, that 
                  // would be the node we are looking for
                  while(tmp2.innerHTML.indexOf(strange_char) == -1){
                      tmp2 = tmp2.parentNode; 
                  }                           
                  // remove that dirty span
                  tmp1.removeChild(span);
                  return tmp2;
              }
          

          【讨论】:

          • 更脏的解决方案的目的是什么? kic(保持复杂)原理?
          【解决方案14】:

          纯JS

          function getFirstCommonAncestor(nodeA, nodeB) {
              const parentsOfA = this.getParents(nodeA);
              const parentsOfB = this.getParents(nodeB);
              return parentsOfA.find((item) => parentsOfB.indexOf(item) !== -1);
          }
          
          function getParents(node) {
              const result = [];
              while (node = node.parentElement) {
                  result.push(node);
              }
              return result;
          }
          

          【讨论】:

            【解决方案15】:

            不喜欢上面的任何答案(想要纯 javascript 和一个函数)。 这对我来说非常有效,高效且更易于理解:

                const findCommonAncestor = (elem, elem2) => {
                  let parent1 = elem.parentElement,parent2 = elem2.parentElement;
                  let childrensOfParent1 = [],childrensOfParent2 = [];
                  while (parent1 !== null && parent2 !== null) {
                    if (parent1 !== !null) {
                      childrensOfParent2.push(parent2);
                      if (childrensOfParent2.includes(parent1)) return parent1;
                    }
                    if (parent2 !== !null) {
                      childrensOfParent1.push(parent1);
                      if (childrensOfParent1.includes(parent2)) return parent2;
                    }
                    parent1 = parent1.parentElement;
                    parent2 = parent1.parentElement;
                  }
                  return null;
                };
            
            

            【讨论】:

              猜你喜欢
              • 2021-09-05
              • 2011-09-01
              • 1970-01-01
              • 1970-01-01
              • 2010-12-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-12-11
              相关资源
              最近更新 更多