【问题标题】:Get child node index获取子节点索引
【发布时间】:2011-08-20 07:30:36
【问题描述】:

在直接的 javascript 中(即没有 jQuery 等扩展),有没有一种方法可以在不迭代和比较所有子节点的情况下确定子节点在其父节点内的索引?

例如,

var child = document.getElementById('my_element');
var parent = child.parentNode;
var childNodes = parent.childNodes;
var count = childNodes.length;
var child_index;
for (var i = 0; i < count; ++i) {
  if (child === childNodes[i]) {
    child_index = i;
    break;
  }
}

有没有更好的方法来确定孩子的索引?

【问题讨论】:

  • 对不起,我是个十足的傻瓜吗?这里有很多看似学到的答案,但要获得所有子节点,您不需要做parent.childNodes,而不是parent.children?。后者仅列出Elements,特别不包括Text 节点...这里的一些答案,例如使用previousSibling,基于使用所有子节点,而其他人只关心Elements ...(!)
  • @mikerodent 我不记得我最初问这个问题的目的是什么,但这是一个我不知道的关键细节。除非你小心,否则绝对应该使用.childNodes 而不是.children。正如您所指出的,前 2 个发布的答案会给出不同的结果。
  • 当计划对 1000 多个节点进行数千次查找时,然后将信息附加到节点(例如通过 child.dataset)。目标是将 O(n) 或 O(n^2) 算法转换为 O(1) 算法。不利的一面是,如果定期添加和删除节点,则附加到节点的相关位置信息也必须更新,这可能不会带来任何性能提升。偶尔的迭代不是什么大问题(例如单击处理程序),但重复迭代是有问题的(例如 mousemove)。

标签: javascript dom


【解决方案1】:

我已经开始喜欢使用 indexOf 来实现这一点。因为indexOfArray.prototype 上,而parent.childrenNodeList,所以你必须使用call(); 这有点难看,但它是一个单行,并且使用任何javascript 开发人员无论如何都应该熟悉的功能。

var child = document.getElementById('my_element');
var parent = child.parentNode;
// The equivalent of parent.children.indexOf(child)
var index = Array.prototype.indexOf.call(parent.children, child);

【讨论】:

  • var index = [].indexOf.call(child.parentNode.children, child);
  • Fwiw,使用 [] 每次运行该代码时都会创建一个 Array 实例,与使用 Array.prototype 相比,这对于内存和 GC 的效率较低。
  • @ScottMiles 我可以再解释一下你所说的吗? [] 不会将内存作为垃圾值清理干净吗?
  • 要评估[].indexOf,引擎必须创建一个数组实例才能访问原型上的indexOf 实现。实例本身未使用(它执行 GC,它不是泄漏,它只是在浪费周期)。 Array.prototype.indexOf 直接访问该实现而不分配匿名实例。几乎在所有情况下,差异都可以忽略不计,所以坦率地说,它可能不值得关心。
  • 当心IE中的错误! Internet Explorer 6、7 和 8 支持它,但错误地包括注释节点。来源"developer.mozilla.org/en-US/docs/Web/API/ParentNode/…
【解决方案2】:

ES6:

Array.from(element.parentNode.children).indexOf(element)

说明:

  • element.parentNode.children → 返回element 的兄弟,包括那个元素。

  • Array.from → 将children 的构造函数转换为Array 对象

  • indexOf → 您可以申请indexOf,因为您现在有一个Array 对象。

【讨论】:

  • 迄今为止最优雅的解决方案 :)
  • 但只能在 Chrome 45 和 Firefox 32 上使用,在 Internet Explorer 上根本不可用。
  • Internet Explorer 还活着吗? Just Jock .. 好的,所以你需要一个 polyfill 才能使 Array.from 在 Internet Explorer 上工作
  • 根据 MDN,调用 Array.from() creates a new Array instance from an array-like or iterable object. 创建一个新的数组实例只是为了找到一个索引可能不是内存或 GC 效率,这取决于操作的频率,在这种情况下迭代,如在接受的答案中解释,会更理想。
  • @TheDarkIn1978 我知道代码优雅和应用程序性能之间存在权衡 ??
【解决方案3】:

您可以使用previousSibling 属性遍历兄弟姐妹,直到您返回null 并计算您遇到的兄弟姐妹数量:

var i = 0;
while( (child = child.previousSibling) != null ) 
  i++;
//at the end i will contain the index.

请注意,在 Java 等语言中,有一个 getPreviousSibling() 函数,但在 JS 中,这已成为一个属性 -- previousSibling

使用previousElementSiblingnextElementSibling 忽略文本和评论节点。

【讨论】:

  • 是的。不过,您在文本中留下了 getPreviousSibling()。
  • 这种方法需要相同数量的迭代来确定子索引,所以我看不出它会更快。
  • 一行版本:for (var i=0; (node=node.previousSibling); i++);
  • @sfarbota Javascript 不知道块作用域,所以i 可以访问。
  • @nepdev 那是因为.previousSibling.previousElementSibling 之间的差异。前者命中文本节点,后者不命中。
【解决方案4】:

ES - 更短

[...element.parentNode.children].indexOf(element);

spread 运算符是一个快捷方式

【讨论】:

  • 这是一个有趣的运算符。
  • e.parentElement.childNodese.parentNode.children有什么区别?
  • childNodes 也包括文本节点
  • 使用 Typescript 你会得到Type 'NodeListOf&lt;ChildNode&gt;' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)
【解决方案5】:

添加一个(为安全起见)element.getParentIndex():

Element.prototype.PREFIXgetParentIndex = function() {
  return Array.prototype.indexOf.call(this.parentNode.children, this);
}

【讨论】:

  • Web 开发的痛苦是有原因的:prefix-jumpy dev 的。为什么不直接做if (!Element.prototype.getParentIndex) Element.prototype.getParentIndex = function(){ /* code here */ }?无论如何,如果这在未来被实施到标准中,那么它可能会被实施为像element.parentIndex 这样的吸气剂。所以,我想说最好的方法是if(!Element.prototype.getParentIndex) Element.prototype.getParentIndex=Element.prototype.parentIndex?function() {return this.parentIndex}:function() {return Array.prototype.indexOf.call(this.parentNode.children, this)}
  • 因为未来的getParentIndex() 可能与您的实现有不同的签名。
  • 跳过辩论,不要做原型污染。一个普通的旧函数没有错。
  • Pony fills 比污染您不拥有的代码安全得多。 function getIndexFromParent(node){...}
  • @JuanMendes 是的,如果您对函数而不是方法感到满意,那么 ECMA265 委员会极不可能添加带有您前缀的方法。
【解决方案6】:

????? ?? ? ???? ????????? ?????? ??

我假设给定一个元素,其中所有子元素在文档上按顺序排列,最快的方法应该是进行二分搜索,比较元素的文档位置。然而,正如结论中介绍的那样,该假设被拒绝。你拥有的元素越多,表现的潜力就越大。例如,如果您有 256 个元素,那么(理想情况下)您只需要检查其中的 16 个!对于65536,只有256!性能增长到 2 的幂!查看更多数字/统计数据。访问Wikipedia

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndex', {
      get: function() {
        var searchParent = this.parentElement;
        if (!searchParent) return -1;
        var searchArray = searchParent.children,
            thisOffset = this.offsetTop,
            stop = searchArray.length,
            p = 0,
            delta = 0;
        
        while (searchArray[p] !== this) {
            if (searchArray[p] > this)
                stop = p + 1, p -= delta;
            delta = (stop - p) >>> 1;
            p += delta;
        }
        
        return p;
      }
    });
})(window.Element || Node);

然后,您使用它的方式是获取任何元素的“parentIndex”属性。例如,查看以下演示。

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndex', {
      get: function() {
        var searchParent = this.parentNode;
        if (searchParent === null) return -1;
        var childElements = searchParent.children,
            lo = -1, mi, hi = childElements.length;
        while (1 + lo !== hi) {
            mi = (hi + lo) >> 1;
            if (!(this.compareDocumentPosition(childElements[mi]) & 0x2)) {
                hi = mi;
                continue;
            }
            lo = mi;
        }
        return childElements[hi] === this ? hi : -1;
      }
    });
})(window.Element || Node);

output.textContent = document.body.parentIndex;
output2.textContent = document.documentElement.parentIndex;
Body parentIndex is <b id="output"></b><br />
documentElements parentIndex is <b id="output2"></b>

限制

  • 此解决方案的实现不适用于 IE8 及更低版本。

在 200,000 个元素上进行二进制 VS 线性搜索(可能会导致某些移动浏览器崩溃,当心!):

  • 在这个测试中,我们将看到线性搜索与二分搜索相比需要多长时间才能找到中间元素。为什么是中间元素?因为它位于所有其他位置的平均位置,所以它最能代表所有可能的位置。

二分查找

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndexBinarySearch', {
      get: function() {
        var searchParent = this.parentNode;
        if (searchParent === null) return -1;
        var childElements = searchParent.children,
            lo = -1, mi, hi = childElements.length;
        while (1 + lo !== hi) {
            mi = (hi + lo) >> 1;
            if (!(this.compareDocumentPosition(childElements[mi]) & 0x2)) {
                hi = mi;
                continue;
            }
            lo = mi;
        }
        return childElements[hi] === this ? hi : -1;
      }
    });
})(window.Element || Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99.9e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=200 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99.9e+3+i+Math.random())).parentIndexBinarySearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the binary search ' + ((end-start)*10).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
}, 125);
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

向后(`lastIndexOf`)线性搜索

(function(t){"use strict";var e=Array.prototype.lastIndexOf;Object.defineProperty(t.prototype,"parentIndexLinearSearch",{get:function(){return e.call(t,this)}})})(window.Element||Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=2000 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99e+3+i+Math.random())).parentIndexLinearSearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the backwards linear search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

前向(`indexOf`)线性搜索

(function(t){"use strict";var e=Array.prototype.indexOf;Object.defineProperty(t.prototype,"parentIndexLinearSearch",{get:function(){return e.call(t,this)}})})(window.Element||Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=2000 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99e+3+i+Math.random())).parentIndexLinearSearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the forwards linear search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

PreviousElementSibling 计数器搜索

计算 PreviousElementSiblings 的数量以获得 parentIndex。

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndexSiblingSearch', {
      get: function() {
        var i = 0, cur = this;
        do {
            cur = cur.previousElementSibling;
            ++i;
        } while (cur !== null)
        return i; //Returns 3
      }
    });
})(window.Element || Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99.95e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=100 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99.95e+3+i+Math.random())).parentIndexSiblingSearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the PreviousElementSibling search ' + ((end-start)*20).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

无搜索

如果浏览器优化了搜索,测试的结果是什么。

test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=2000 + end; i-- !== end; )
    console.assert( true );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the no search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden></div>

结论

但是,在 Chrome 中查看结果后,结果与预期相反。更笨的前向线性搜索是惊人的 187 毫秒,比二分搜索快 3850%。显然,Chrome 不知何故神奇地超过了console.assert 并对其进行了优化,或者(更乐观地)Chrome 在内部使用数字索引系统来处理 DOM,并且这个内部索引系统通过在 Array.prototype.indexOf 上使用时的优化暴露出来。 987654337@对象。

【讨论】:

  • 高效,但不切实际。
  • 谈过早优化。抱歉,但这值得一票否决...您为什么要费心优化这种通常不会成为瓶颈的简单查找?如果您有包含数千个子节点的节点,那么您可能做错了。
  • 我猜想 childNodes 集合在引擎中是作为链表实现的,因此为什么二进制搜索不会有效地工作。这就解释了为什么previousSibling 是一件事而parentIndex 不是。
【解决方案7】:

你可以这样做吗:

var index = Array.prototype.slice.call(element.parentElement.children).indexOf(element);

https://developer.mozilla.org/en-US/docs/Web/API/Node/parentElement

【讨论】:

    【解决方案8】:

    当节点有大量兄弟节点时,使用binary search algorithm 来提高性能。

    function getChildrenIndex(ele){
        //IE use Element.sourceIndex
        if(ele.sourceIndex){
            var eles = ele.parentNode.children;
            var low = 0, high = eles.length-1, mid = 0;
            var esi = ele.sourceIndex, nsi;
            //use binary search algorithm
            while (low <= high) {
                mid = (low + high) >> 1;
                nsi = eles[mid].sourceIndex;
                if (nsi > esi) {
                    high = mid - 1;
                } else if (nsi < esi) {
                    low = mid + 1;
                } else {
                    return mid;
                }
            }
        }
        //other browsers
        var i=0;
        while(ele = ele.previousElementSibling){
            i++;
        }
        return i;
    }
    

    【讨论】:

    • 不起作用。我不得不指出,IE 版本和“其他浏览器”版本会计算不同的结果。 “其他浏览器”技术按预期工作,获得父节点下的第 n 个位置,但是 IE 技术“检索对象的序号位置,按源顺序,因为对象出现在文档的所有集合中”(msdn.microsoft.com/en-gb/library/ie/ms534635(v=vs.85).aspx )。例如。我使用“IE”技术获得了 126 个,然后使用另一个获得了 4 个。
    【解决方案9】:

    我遇到了文本节点问题,它显示了错误的索引。这是修复它的版本。

    function getChildNodeIndex(elem)
    {   
        let position = 0;
        while ((elem = elem.previousSibling) != null)
        {
            if(elem.nodeType != Node.TEXT_NODE)
                position++;
        }
    
        return position;
    }
    

    【讨论】:

      【解决方案10】:

      如果您的元素是&lt;tr/&gt;&lt;td/&gt;,请使用rowIndex/cellIndex 属性。

      【讨论】:

        【解决方案11】:
        Object.defineProperties(Element.prototype,{
        group : {
            value: function (str, context) {
                // str is valid css selector like :not([attr_name]) or .class_name
                var t = "to_select_siblings___";
                var parent = context ? context : this.parentNode;
                parent.setAttribute(t, '');
                var rez = document.querySelectorAll("[" + t + "] " + (context ? '' : ">") + this.nodeName + (str || "")).toArray();
                parent.removeAttribute(t);            
                return rez;  
            }
        },
        siblings: {
            value: function (str, context) {
                var rez=this.group(str,context);
                rez.splice(rez.indexOf(this), 1);
                return rez; 
            }
        },
        nth: {  
            value: function(str,context){
               return this.group(str,context).indexOf(this);
            }
        }
        }
        

        /* html */
        <ul id="the_ul">   <li></li> ....<li><li>....<li></li>   </ul>
        
         /*js*/
         the_ul.addEventListener("click",
            function(ev){
               var foo=ev.target;
               foo.setAttribute("active",true);
               foo.siblings().map(function(elm){elm.removeAttribute("active")});
               alert("a click on li" + foo.nth());
             });
        

        【讨论】:

        • 你能解释一下为什么你从Element.prototype扩展吗?这些函数看起来很有用,但我不知道这些函数的作用(即使命名很明显)。
        • @extend Element.prototype 原因是相似性... 4 ex elemen.children , element.parentNode 等...所以您处理 element.siblings 的方式相同 .... group 方法是有点复杂,因为我想通过相同的 nodeType 将兄弟方法扩展到类似的元素,并且具有相同的属性,即使没有相同的祖先
        • 我知道原型扩展是什么,但我想知道您的代码是如何使用的。 el.group.value() ??。我的第一条评论是为了提高您的回答质量。
        • 组和兄弟方法返回带有已建立 dom 元素的数组 .. .... 感谢您的评论和评论的原因
        • 非常优雅,但也很慢。
        【解决方案12】:
        <body>
            <section>
                <section onclick="childIndex(this)">child a</section>
                <section onclick="childIndex(this)">child b</section>
                <section onclick="childIndex(this)">child c</section>
            </section>
            <script>
                function childIndex(e){
                    let i = 0;
                    while (e.parentNode.children[i] != e) i++;
                    alert('child index '+i);
                }
            </script>
        </body>
        

        【讨论】:

        • 这里不需要 jQuery。
        • @VitalyZdanevich 说得对,但这也可以作为谁使用的解决方案。
        【解决方案13】:

        对我来说,这段代码更清晰

        const myElement = ...;
        const index = [...document.body.children].indexOf(myElement);
        

        【讨论】:

        • 这与philipp 的回答有何不同?您正在从孩子创建一个数组并找到索引。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-14
        • 1970-01-01
        • 2020-10-21
        • 1970-01-01
        相关资源
        最近更新 更多