Obtener índice de nodo secundario


125

En javascript directo (es decir, sin extensiones como jQuery, etc.), ¿hay alguna manera de determinar el índice de un nodo secundario dentro de su nodo principal sin iterar y comparar todos los nodos secundarios?

P.ej,

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

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


2
Lo siento, ¿soy un completo idiota? Aquí hay muchas respuestas aparentemente aprendidas, pero para obtener todos los nodos secundarios, ¿no es necesario hacer parent.childNodes, en lugar de parent.children?. Este último solo enumera los Elements, excluyendo en particular los Textnodos ... Algunas de las respuestas aquí, por ejemplo, el uso previousSibling, se basan en el uso de todos los nodos secundarios, mientras que otras solo se preocupan por los niños que son Elements ... (!)
Mike rodent

@mikerodent No recuerdo cuál era mi propósito cuando inicialmente hice esta pregunta, pero ese es un detalle clave que no conocía. A menos que tenga cuidado, .childNodesdefinitivamente debería usarse en lugar de .children. Las 2 respuestas principales publicadas darán resultados diferentes, como señaló.
Todos los trabajadores son esenciales

Cuando planee realizar miles de búsquedas en más de 1000 nodos, adjunte información al nodo (por ejemplo, a través de child.dataset). El objetivo sería convertir un algoritmo O (n) u O (n ^ 2) en un algoritmo O (1). La desventaja es que si los nodos se agregan y eliminan con regularidad, la información de posición asociada adjunta a los nodos también tendrá que actualizarse, lo que podría no generar ninguna mejora en el rendimiento. La iteración ocasional no es un gran problema (por ejemplo, el controlador de clics), pero la repetición es problemática (por ejemplo, mousemove).
CubicleSoft

Respuestas:


122

puede usar la previousSiblingpropiedad para iterar a través de los hermanos hasta que regrese nully cuente cuántos hermanos ha 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, hay una getPreviousSibling()función, sin embargo en JS esto se ha convertido en una propiedad - previousSibling.


2
Sí. Sin embargo, ha dejado un getPreviousSibling () en el texto.
Tim Down

7
este enfoque requiere la misma cantidad de iteraciones para determinar el índice secundario, por lo que no puedo ver cómo sería mucho más rápido.
Michael

31
Versión de una línea:for (var i=0; (node=node.previousSibling); i++);
Scott Miles

2
@sfarbota Javascript no conoce el alcance del bloque, por ilo que será accesible.
A1rPun

3
@nepdev Eso se debe a las diferencias entre .previousSiblingy .previousElementSibling. El primero llega a los nodos de texto, el segundo no.
abluejelly

133

Me ha gustado usar indexOfpara esto. Debido a que indexOfestá encendido Array.prototypey parent.childrenes a NodeList, tienes que usar call();Es un poco feo, pero es una línea y usa 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);

7
var index = [] .indexOf.call (child.parentNode.children, child);
cuixiping

23
Fwiw, el uso []crea una instancia de Array cada vez que ejecuta ese código, que es menos eficiente para la memoria y GC que el uso Array.prototype.
Scott Miles

@ScottMiles ¿Puedo pedir que me explique un poco más lo que ha dicho? ¿No []se limpia la memoria como valor basura?
mrReiha

14
Para evaluar [].indexOfel motor hay que crear una instancia de matriz solo para acceder a la indexOfimplementación en el prototipo. La instancia en sí no se usa (hace GC, no es una fuga, solo está desperdiciando ciclos). Array.prototype.indexOfaccede a esa implementación directamente sin asignar una instancia anónima. La diferencia será insignificante en casi todas las circunstancias, por lo que, francamente, puede que no valga la pena preocuparse por ella.
Scott Miles

3
¡Cuidado con los errores en IE! Internet Explorer 6, 7 y 8 lo admiten, pero incluye erróneamente nodos de comentario. Fuente " developer.mozilla.org/en-US/docs/Web/API/ParentNode/…
Luckylooke

102

ES6:

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

Explicación:

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

  • Array.from→ Lanza el constructor de childrena un Arrayobjeto

  • indexOf→ Puede postularse indexOfporque ahora tiene un Arrayobjeto.


2
La solución más elegante, de lejos :)
Amit Saxena

1
Pero solo está disponible en Chrome 45 y Firefox 32, y no está disponible en Internet Explorer.
Peter

Internet Explorer sigue vivo? Just Jock .. Ok, entonces necesitas un polyfill para hacer Array.fromtrabajos en Internet Explorer
Abdennour TOUMI

9
Según MDN, llamar a Array.from () creates a new Array instance from an array-like or iterable object. Crear una nueva instancia de matriz solo para encontrar un índice podría no ser eficiente en memoria o GC, dependiendo de la frecuencia de la operación, en cuyo caso la iteración, como se explica en la respuesta aceptada, sería más ideal.
TheDarkIn1978

1
@ TheDarkIn1978 Soy consciente de que hay un intercambio entre la elegancia del código y el rendimiento de la aplicación 👍🏻
Abdennour TOUMI

38

ES: más corto

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

El operador de propagación es un atajo para eso


Ese es un operador interesante.
Todos los trabajadores son esenciales

¿Cuál es la diferencia entre e.parentElement.childNodesy e.parentNode.children?
mesqueeb

4
childNodestambién incluye nodos de texto
philipp

Con Type 'NodeListOf<ChildNode>' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)
TypeScript

9

Añadiendo un elemento (prefijado por seguridad) .getParentIndex ():

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

1
Hay una razón para el dolor del desarrollo web: los desarrolladores con prefijo-saltos. ¿Por qué no hacerlo if (!Element.prototype.getParentIndex) Element.prototype.getParentIndex = function(){ /* code here */ }? De todos modos, si esto se implementa alguna vez en el estándar en el futuro, es probable que se implemente como un getter element.parentIndex. Entonces, diría que el mejor enfoque seríaif(!Element.prototype.getParentIndex) Element.prototype.getParentIndex=Element.prototype.parentIndex?function() {return this.parentIndex}:function() {return Array.prototype.indexOf.call(this.parentNode.children, this)}
Jack Giffin

3
Porque el futuro getParentIndex()puede tener una firma diferente a su implementación.
mikemaccana

7

Supongo que dado un elemento en el que todos sus elementos secundarios están ordenados en el documento secuencialmente, la forma más rápida debería ser hacer una búsqueda binaria, comparando las posiciones del documento de los elementos. Sin embargo, como se introdujo en la conclusión, se rechaza la hipótesis. Cuantos más elementos tenga, mayor será el potencial de rendimiento. Por ejemplo, si tuviera 256 elementos, entonces (de manera óptima) ¡solo necesitaría verificar solo 16 de ellos! ¡Para 65536, solo 256! ¡El rendimiento crece a la potencia de 2! Ver más números / estadísticas. Visite 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 en que lo usa es obteniendo la propiedad 'parentIndex' de cualquier elemento. Por ejemplo, consulte 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.

Binary VS Linear Search en 200 mil elementos (puede fallar algunos navegadores móviles, ¡CUIDADO!):

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

Búsqueda binaria

Búsqueda lineal hacia atrás (`lastIndexOf`)

Búsqueda lineal hacia adelante (`indexOf`)

AnteriorElementSibling Counter Search

Cuenta el número de PreviousElementSiblings para obtener parentIndex.

Sin búsqueda

Para comparar cuál sería el resultado de la prueba si el navegador optimizara la búsqueda.

La Conculsion

Sin embargo, después de ver los resultados en Chrome, los resultados son opuestos a lo esperado. La búsqueda lineal hacia adelante más tonta fue un sorprendente 187 ms, 3850%, más rápida que la búsqueda binaria. Evidentemente, Chrome de alguna manera superó mágicamente al console.asserty lo optimizó, o (de manera más optimista) 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 Array.prototype.indexOfcuando se usa en un HTMLCollectionobjeto.


Eficiente, pero poco práctico.
applemonkey496

3

Utilice el algoritmo de búsqueda binaria para mejorar el rendimiento cuando el nodo tiene muchos hermanos.

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

No funciona. Me veo obligado a señalar que la versión de IE y la versión de "otro navegador" calcularán resultados diferentes. La técnica de "otros navegadores" funciona como se esperaba, obteniendo la enésima posición debajo del nodo principal, sin embargo, la técnica de IE "Recupera la posición ordinal del objeto, en orden de origen, a medida que el objeto aparece en toda la colección del documento" ( msdn.microsoft .com / en-gb / library / ie / ms534635 (v = frente a 85) .aspx ). Por ejemplo, obtuve 126 usando la técnica "IE", y luego 4 usando la otra.
Christopher Bull

1

Tuve un problema con los nodos de texto y mostraba un índice incorrecto. Aquí hay una versión para solucionarlo.

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

    return position;
}

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

Ex

/* 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());
     });

1
¿Puede explicar por qué se extiende desde Element.prototype? Las funciones parecen útiles, pero no sé qué hacen estas funciones (incluso si los nombres son obvios).
A1rPun

@ extender Element.prototype la razón es la similitud ... 4 ex elemen.children, element.parentNode etc ... así que de la misma manera que te diriges a element.siblings ... el método de grupo es un poco complicado porque quiero extender un poco el enfoque de hermanos a los elelemts por el mismo tipo de nodo y con los mismos atributos, incluso sin tener el mismo ancestro
bortunac

Sé lo que es la extensión de prototipos, pero me gusta saber cómo se usa su código. el.group.value()??. Mi primer comentario está ahí para mejorar la calidad de su respuesta.
A1rPun

los métodos de grupo y hermanos devuelven Array con elementos dom fundados .. .... gracias por su comentario y por la razón del comentario
bortunac

Muy elegante, pero también muy lento.
Jack Giffin


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

No necesitas jQuery aquí.
Vitaly Zdanevich
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.