Búsqueda de sitios web

javascript - Obtener el índice del nodo hijo


En JavaScript puro (es decir, sin extensiones como jQuery, etc.), ¿hay alguna manera de determinar el índice de un nodo hijo dentro de su nodo padre sin iterar y comparar todos los nodos hijos?

Por ejemplo,

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

¿Hay una mejor manera de determinar el índice del niño?

Respuesta #1

Me he encariñado con usar indexOf para esto. Debido a que indexOf está en Array.prototype y parent.children es un NodeList, tienes que usar call();. Es un poco feo, pero es una línea y utiliza funciones con las que cualquier desarrollador de JavaScript debería estar familiarizado de todos modos.

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

Respuesta #2

ES6:

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

Explicación :

  • element.parentNode.children → Devuelve los hermanos de element, incluido ese elemento.

  • Array.from → Convierte el constructor de children en un objeto Array

  • indexOf → Puedes aplicar indexOf porque ahora tienes un objeto Array.

Respuesta #3

puedes usar la propiedad previousSibling para iterar hacia atrás a través de los hermanos hasta que vuelvas a null y contar cuántos hermanos has encontrado:

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

Tenga en cuenta que en lenguajes como Java, existe una función getPreviousSibling(), sin embargo, en JS esto se ha convertido en una propiedad: previousSibling.

Usa previousElementSibling o nextElementSibling para ignorar los nodos de texto y comentario.

Respuesta #4

ES—Más corto

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

El operador spread es un atajo para eso

Respuesta #5

Prueba de una búsqueda binaria menos eficiente

Hipotetizo que dado un elemento donde todos sus hijos están ordenados secuencialmente en el documento, la forma más rápida debería ser hacer una búsqueda binaria, comparando las posiciones de los elementos en el documento. Sin embargo, como se indicó en la conclusión, la hipótesis es rechazada. Cuantos más elementos tengas, mayor es el potencial de rendimiento. Por ejemplo, si tuvieras 256 elementos, entonces (óptimamente) ¡solo necesitarías verificar 16 de ellos! Para 65536, ¡solo 256! ¡El rendimiento crece a la potencia de 2! Consulta más números/estadísticas. Visita 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);

Entonces, la forma de usarlo es obteniendo la propiedad 'parentIndex' de cualquier elemento. Por ejemplo, mira la siguiente demostración.

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

Limitaciones

  • Esta implementación de la solución no funcionará en IE8 y versiones anteriores.

Búsqueda Binaria VS Búsqueda Lineal en 200,000 elementos (¡podría bloquear algunos navegadores móviles, CUIDADO!):

  • En esta prueba, veremos cuánto tiempo tarda una búsqueda lineal en encontrar el elemento del medio frente a una búsqueda binaria. ¿Por qué el elemento del medio? Porque se encuentra en la ubicación promedio de todas las demás ubicaciones, por lo que representa mejor todas las posibles ubicaciones.

Búsqueda binaria

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

Búsqueda Lineal hacia atrás (`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>

Búsqueda lineal hacia adelante (`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>

Búsqueda del contador del hermano anterior

Cuenta el número de elementos hermanos anteriores para obtener el índice del padre.

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

Sin búsqueda

Para evaluar qué resultado tendría la prueba si el navegador optimizara la búsqueda.

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>

La conclusión

Sin embargo, después de ver los resultados en Chrome, los resultados son lo opuesto a lo esperado. La búsqueda lineal más torpe fue sorprendentemente de 187 ms, un 3850% más rápida que la búsqueda binaria. Evidentemente, de alguna manera Chrome superó mágicamente a console.assert y la optimizó, o (más optimistamente) Chrome utiliza internamente un sistema de indexación numérica para el DOM, y este sistema de indexación interno se expone a través de las optimizaciones aplicadas a Array.prototype.indexOf cuando se usa en un objeto HTMLCollection.

Respuesta #6

¿Podrías hacer algo así:

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

https://developer.mozilla.org/es/docs/Web/API/Node/parentElement

Artículos relacionados