如何找到两个或更多节点最近的共同祖先?

时间:2010-10-18 15:50:02

标签: javascript jquery

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

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

谢谢。

15 个答案:

答案 0 :(得分:65)

这是一个更高效的纯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]
  }
}

答案 1 :(得分:43)

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

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

jsFiddle example

答案 2 :(得分:8)

试试这个:

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等),应该很容易修改。

答案 3 :(得分:8)

这是另一个使用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

答案 4 :(得分: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。

答案 5 :(得分:3)

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

答案 6 :(得分:2)

您还可以使用DOM Range(当然,当浏览器支持时)。如果您创建Range startContainer,并将endContainer设置为文档中的早期节点,并将Range设置为文档中的后一个节点,那么commonAncestorContainer 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; } 是最深的共同祖先节点。

以下是实现此想法的一些代码:

{{1}}

答案 7 :(得分:1)

这是对寂寞回答的概括。它不仅需要两个元素,而且需要一个完整的JQuery对象。

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

答案 8 :(得分:1)

派对有点晚了,但这是一个优雅的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();
};

答案 9 :(得分:1)

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

步骤进行:

  1. 抓住node_a的父级
  2. 如果node_a的父节点包含node_b返回父节点(在我们的代码中,父节点被引用为node_a)
  3. 如果父级不包含node_b,我们需要继续保持链上升
  4. end return null
  5. 代码:

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

答案 10 :(得分:1)

晚了一点,这是一个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);

答案 11 :(得分:1)

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

答案 12 :(得分:0)

这是一种更脏的方式。它更容易理解,但需要修改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;
    }

答案 13 :(得分:0)

PureJS

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

答案 14 :(得分:0)

不喜欢以上任何答案(想要纯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;
    };