218

In straight up javascript (i.e., no extensions such as jQuery, etc.), is there a way to determine a child node's index inside of its parent node without iterating over and comparing all children nodes?

E.g.,

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

Is there a better way to determine the child's index?

3
  • 2
    Sorry, am I a complete dolt? There are lots of seemingly learned answers here, but to get all the children nodes don't you need to do parent.childNodes, rather than parent.children?. The latter only lists the Elements, excluding in particular Text nodes... Some of the answers here, e.g. using previousSibling, are based on using all child nodes, whereas others are only bothered with children which are Elements... (!) Commented Oct 12, 2017 at 11:17
  • @mikerodent I don't remember what my purpose was when I initially asked this question, but that's a key detail that I wasn't aware of. Unless you're careful, .childNodes should definitely be used instead of .children. The top 2 posted answers will give different results as you pointed out. Commented Oct 12, 2017 at 13:49
  • 1
    When planning on doing thousands of lookups over 1000+ nodes, then attach information to the node (e.g. via child.dataset). The goal would be to convert a O(n) or O(n^2) algorithm into a O(1) algorithm. The downside is that if nodes are added and removed regularly, the associated position information attached to the nodes will have to be updated too, which might not result in any performance gains. The occasional iteration isn't a big deal (e.g. click handler) but repeated iteration is problematic (e.g. mousemove). Commented May 3, 2020 at 13:09

12 Answers 12

209

I've become fond of using indexOf for this. Because indexOf is on Array.prototype and parent.children is a NodeList, you have to use call(); It's kind of ugly but it's a one liner and uses functions that any javascript dev should be familiar with anyhow.

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); 
Sign up to request clarification or add additional context in comments.

9 Comments

var index = [].indexOf.call(child.parentNode.children, child);
Fwiw, using [] creates an Array instance every time you run that code, which is less efficient for memory and GC vs using Array.prototype.
To evaluate [].indexOf the engine has to create an array instance just to access the indexOf implementation on the prototype. The instance itself goes unused (it does GC, it's not a leak, it's just wasting cycles). Array.prototype.indexOf accesses that implementation directly without allocating an anonymous instance. The difference is going to be negligible in almost all circumstances, so frankly it may not be worth caring about.
Beware of error in IE! Internet Explorer 6, 7 and 8 supported it, but erroneously includes Comment nodes. Source" developer.mozilla.org/en-US/docs/Web/API/ParentNode/…
in modern javascript you can use [ ...parent.children ].indexOf ( child )
|
201

ES6:

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

Explanation :

  • element.parentNode.children → Returns the brothers of element, including that element.

  • Array.from → Casts the constructor of children to an Array object

  • indexOf → You can apply indexOf because you now have an Array object.

3 Comments

Internet Explorer still alive ? Just Jock .. Ok so you need a polyfill to make Array.from works on Internet explorer
According to MDN, calling Array.from() creates a new Array instance from an array-like or iterable object. Creating a new array instance just to find an index might not be memory or GC efficient, depending on how frequent the operation, in which case iteration, as explained in the accepted answer, would be more ideal.
@TheDarkIn1978 I am aware there is a trades-off between code elegance & app performance 👍🏻
160

you can use the previousSibling property to iterate back through the siblings until you get back null and count how many siblings you've encountered:

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

Please note that in languages like Java, there is a getPreviousSibling() function, however in JS this has become a property -- previousSibling.

Use previousElementSibling or nextElementSibling to ignore text and comment nodes.

15 Comments

Yep. You've left a getPreviousSibling() in the text though.
this approach requires the same number of iterations to determine the child index, so i can't see how it would be much faster.
One line version: for (var i=0; (node=node.previousSibling); i++);
@sfarbota Javascript doesn't know block scoping, so i will be accesible.
@nepdev That would be because of the differences between .previousSibling and .previousElementSibling. The former hits text nodes, the latter doesn't.
|
69

ES—Shorter

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

The spread Operator is a shortcut for that

4 Comments

What's the difference between e.parentElement.childNodes and e.parentNode.children ?
childNodes includes text nodes as well
With Typescript you get Type 'NodeListOf<ChildNode>' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)
When using Typescript, you need to update your tsconfig.json to import the correct interfaces. "lib": ["dom", "dom.iterable"] . Make sure you include dom.iterable.
13

𝗣𝗿𝗼𝗼𝗳 𝗢𝗳 𝗔 𝗟𝗲𝘀𝘀 𝗘𝗳𝗳𝗶𝗰𝗶𝗲𝗻𝘁 𝗕𝗶𝗻𝗮𝗿𝘆 𝗦𝗲𝗮𝗿𝗰𝗵

I hypothesize that given an element where all of its children are ordered on the document sequentially, the fastest way should be to do a binary search, comparing the document positions of the elements. However, as introduced in the conclusion the hypothesis is rejected. The more elements you have, the greater the potential for performance. For example, if you had 256 elements, then (optimally) you would only need to check just 16 of them! For 65536, only 256! The performance grows to the power of 2! See more numbers/statistics. Visit 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); 

Then, the way that you use it is by getting the 'parentIndex' property of any element. For example, check out the following demo.

(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>

Limitations

  • This implementation of the solution will not work in IE8 and below.

Binary VS Linear Search On 200,000 elements (might crash some mobile browsers, BEWARE!):

  • In this test, we will see how long it takes for a linear search to find the middle element VS a binary search. Why the middle element? Because it is at the average location of all the other locations, so it best represents all of the possible locations.

Binary Search

(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>

Backwards (`lastIndexOf`) Linear Search

(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>

Forwards (`indexOf`) Linear Search

(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 Counter Search

Counts the number of PreviousElementSiblings to get the 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>

No Search

For benchmarking what the result of the test would be if the browser optimized out the searching.

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>

The Conculsion

However, after viewing the results in Chrome, the results are the opposite of what was expected. The dumber forwards linear search was a surprising 187 ms, 3850%, faster than the binary search. Evidently, Chrome somehow magically outsmarted the console.assert and optimized it away, or (more optimistically) Chrome internally uses numerical indexing system for the DOM, and this internal indexing system is exposed through the optimizations applied to Array.prototype.indexOf when used on a HTMLCollection object.

3 Comments

Efficient, but impractical.
Talk about premature optimization. Sorry but this deserves a downvote... Why are you bothering with optimizing such simple lookup that's not often a source of bottlenecks? If you have nodes with thousands of children, you're probably doing it wrong.
I guess the childNodes collection is implemented as a linked list in the engine, hence why binary search won't work efficiently. And that explains why previousSibling is a thing while parentIndex isn't.
10

Could you do something like this:

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

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

Comments

9

Making a getParentIndex(element):

const getParentIndex = function(element) { return Array.prototype.indexOf.call(element.parentNode.children, element); } 

Update: I originally wrote this ten years ago using a prototype extension, with a prefix to avoid conflicts, but since have changed my mind about using prefixed prototypes, and have updated this answer to just make a helper function.

Comments

8

If your element is <tr> or <td>, you can use the rowIndex/cellIndex property.

1 Comment

note that in the case of <tr> elements, this will take into account any table headers you might also have in your table.
4

Use binary search algorithm to improve the performace when the node has large quantity siblings.

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

1 Comment

Doesn't work. I'm compelled to point out that the IE version and "other browser" version will calculate different results. The "other browsers" technique works as expected, getting the nth position under the parent node, however the IE technique "Retrieves the ordinal position of the object, in source order, as the object appears in the document's all collection" ( msdn.microsoft.com/en-gb/library/ie/ms534635(v=vs.85).aspx ). E.g. I got 126 using "IE" technique, and then 4 using the other.
3

I had issue with text nodes, and it was showing wrong index. Here is version to fix it.

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

Comments

-1
Object.defineProperties(Element.prototype,{ group: { value: function (str, context) { var scope = context ? context : this.parentNode; if (!scope || scope === document) return null; return [...scope.querySelectorAll(":scope" + (context ? " " : ">") + this.nodeName + (str || ""))]; } }, siblings: { value: function (str, context) { var rez = this.group(str, context) || []; rez.splice(rez.indexOf(this), 1); return rez; // !!!!!!!!!!!!!!! ATENTIE returneaza Array (nu NodeList) } }, nth: { value: function (str, context) { return this.group(str, context).indexOf(this); } } } 

Ex

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

5 Comments

Can you explain why you extend from Element.prototype? The functions look useful but I don't know what these functions do (even if the namings are obvious).
@ extend Element.prototype the reason is similarity ... 4 ex elemen.children , element.parentNode etc ... so the same way you address element.siblings .... the group method is a little complicated cause I want to extend a little the sibling approach to elelemts alike by the same nodeType and having same attributes even not having same ancestor
I know what prototype extending is but I like to know how is your code used. el.group.value() ??. My first comment is there to improve the quality of your answer.
the group and siblings methods return Array with founded dom elements .. .... thanks for your comment and for the reason of comment
Very elegant, yet also very slow.
-2
<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> 

2 Comments

You do not need jQuery here.
@VitalyZdanevich right, but this also can be a solution for who does use.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.