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

Javascript 如何找到两个或多个节点最近的共同祖先?,javascript,jquery,Javascript,Jquery,用户在HTML页面中选择两个或多个元素。我想完成的是找到这些元素的共同祖先(因此,如果以前没有找到,那么body节点将是共同祖先) 附言:这可以通过XPath实现,但对我来说不是一个更好的选择。也可以通过css选择器解析找到它,但我认为它是一个肮脏的方法(?) 谢谢。您应该能够使用jQuery.parents()函数,然后遍历结果以查找第一个匹配项。(或者我想你可以从头开始,然后向后走,直到你看到第一个区别;这可能更好。)试试这个: function get_common_ancestor(a,

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

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


谢谢。

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

试试这个:

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

这里有一个纯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]
  }
}
函数父节点(节点){
变量节点=[node]
for(;node;node=node.parentNode){
节点。取消移位(节点)
}
返回节点
}
功能公共祖先(节点1、节点2){
var parents1=父节点(节点1)
var parents2=父节点(node2)
如果(parents1[0]!=parents2[0])抛出“无共同祖先!”
对于(变量i=0;i
涉及手动遍历祖先元素的解决方案远比必要的复杂。您不需要手动执行循环。使用获取一个元素的所有祖先元素,将其缩减为包含第二个元素的元素,然后使用获取第一个祖先

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

比较两个节点 工作演示:(使用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;
}
函数getCommonSenator(node1/*、node2、node3、…nodeN*/){ if(arguments.length<2) 抛出新错误(“GetCommonSenator:参数不足”); var i, method=“contains”在节点1中“contains”:“compareDocumentPosition”, 测试=方法==“包含”?1:0x0010, nodes=[].slice.call(参数,1); 摇摆: while(node1=node1.parentNode){ i=节点长度; 而(我--){ if((节点1[方法](节点[i])&测试)!==测试) 继续摇摆; } 返回节点1; } 返回null; } 工作演示:

您还可以使用DOM(当然,当浏览器支持时)。如果创建一个
范围
,将
startContainer
设置为文档中较早的节点,将
endContainer
设置为文档中较后的节点,则该
范围
的最深公共祖先节点

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

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

这是对lonesomeday答案的概括理解。它将采用一个完整的JQuery对象,而不仅仅是两个元素

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

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

/**
*获取一个元素的所有父元素,包括其自身
*@returns{jQuerySelector}
*/
$.fn.family=函数(){
变量i,el,节点=$();
对于(i=0;i
上面提到的he Range API的属性,以及它的特性,使得这一点非常简单

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

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

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

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

步骤:

  • 抓取节点_a的父节点
  • 如果节点_a的父节点包含节点_b返回父节点(在我们的代码中,父节点被引用为节点_a)
  • 如果父节点不包含节点,我们需要继续沿着链向上
  • 结束返回空值
  • 代码:

    PureJS


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

    这是一个JavaScript ES6版本,它使用和,可以使用任意数量的元素作为参数:

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

    根据Andy E
    和AntonB

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

    函数getCommonParentNode(节点1,
    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;
        }
    
    function getLowestCommonParent(node_a, node_b) {
        while (node_a = node_a.parentElement) {
            if (node_a.contains(node_b)) {
                return node_a;
            }
        }
    
        return null;
    }
    
    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;
    }
    
        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;
        };
    
    
    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);