Obter índice de nó filho

125

Em javascript direto (ou seja, sem extensões como jQuery, etc.), há uma maneira de determinar o índice de um nó filho dentro de seu nó pai sem iterar e comparar todos os nós filhos?

Por exemplo,

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 uma maneira melhor de determinar o índice da criança?

Todos os trabalhadores são essenciais
fonte
2
Desculpe, sou um completo idiota? Existem muitas respostas aparentemente aprendidas aqui, mas para obter todos os nós filhos, você não precisa fazer parent.childNodes, em vez de parent.children?. Este último lista apenas o Elements, excluindo em Textnós específicos ... Algumas das respostas aqui, por exemplo, usando previousSibling, são baseadas no uso de todos os nós filhos, enquanto outras são incomodadas apenas com crianças que são Elements ... (!)
roedor de microfone
@mikerodent Não me lembro qual era o meu propósito quando fiz esta pergunta inicialmente, mas esse é um detalhe importante que eu não sabia. A menos que você seja cuidadoso, .childNodesdefinitivamente deve ser usado em vez de .children. As 2 principais respostas postadas fornecerão resultados diferentes, conforme você apontou.
Todos os trabalhadores são essenciais
Ao planejar fazer milhares de pesquisas em mais de 1000 nós, anexe as informações ao nó (por exemplo, via child.dataset). O objetivo seria converter um algoritmo O (n) ou O (n ^ 2) em um algoritmo O (1). A desvantagem é que, se os nós forem adicionados e removidos regularmente, as informações de posição associadas anexadas aos nós também terão que ser atualizadas, o que pode não resultar em nenhum ganho de desempenho. A iteração ocasional não é um grande problema (por exemplo, manipulador de clique), mas a iteração repetida é problemática (por exemplo, mousemove).
CubicleSoft

Respostas:

122

você pode usar a previousSiblingpropriedade para iterar de volta entre os irmãos até voltar nulle contar quantos irmãos encontrou:

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

Observe que em linguagens como Java, existe uma getPreviousSibling()função, no entanto, em JS, ela se tornou uma propriedade - previousSibling.

Liv
fonte
2
Sim. Porém, você deixou getPreviousSibling () no texto.
Tim Down
7
essa abordagem requer o mesmo número de iterações para determinar o índice filho, então não consigo ver como seria muito mais rápido.
Michael
31
Versão de uma linha:for (var i=0; (node=node.previousSibling); i++);
Scott Miles
2
@sfarbota Javascript não conhece o escopo do bloco, então iestará acessível.
A1rPun
3
@nepdev Isso seria devido às diferenças entre .previousSiblinge .previousElementSibling. O primeiro atinge os nós de texto, o último não.
abluejelly
133

Eu gosto de usar indexOfpara isso. Como indexOfestá ativado Array.prototypee parent.childrené um NodeList, você deve usar call();É meio feio, mas é um liner e usa funções com as quais qualquer desenvolvedor de javascript deve estar familiarizado.

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);
KhalilRavanna
fonte
7
var index = [] .indexOf.call (child.parentNode.children, child);
cuixiping
23
Fwiw, usando []cria uma instância de Array toda vez que você executa esse código, o que é menos eficiente para memória e GC do que usar Array.prototype.
Scott Miles
@ScottMiles Posso pedir para explicar um pouco mais o que você disse? Não []limpa a memória como um valor de lixo?
mrReiha
14
Para avaliar [].indexOfo motor é necessário criar uma instância de array apenas para acessar a indexOfimplementação no protótipo. A instância em si fica sem uso (faz GC, não é um vazamento, está apenas desperdiçando ciclos). Array.prototype.indexOfacessa essa implementação diretamente sem alocar uma instância anônima. A diferença será insignificante em quase todas as circunstâncias, portanto, francamente, pode não valer a pena se preocupar com isso.
Scott Miles
3
Cuidado com os erros no IE! O Internet Explorer 6, 7 e 8 o suportava, mas inclui erroneamente nós Comentário. Fonte " developer.mozilla.org/en-US/docs/Web/API/ParentNode/…
Luckylooke
102

ES6:

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

Explicação:

  • element.parentNode.children→ Retorna os irmãos de element, incluindo aquele elemento.

  • Array.from→ Converte o construtor de childrenem um Arrayobjeto

  • indexOf→ Você pode se inscrever indexOfporque agora você tem um Arrayobjeto.

Abdennour TOUMI
fonte
2
A solução mais elegante, de longe :)
Amit Saxena
1
Mas só está disponível no Chrome 45 e Firefox 32, e não está disponível no Internet Explorer.
Peter,
O Internet Explorer ainda está vivo? Just Jock .. Ok, então você precisa de um polyfill para fazer Array.fromtrabalhos no Internet explorer
Abdennour TOUMI
9
De acordo com MDN, chamar Array.from () creates a new Array instance from an array-like or iterable object. Criar uma nova instância de array apenas para encontrar um índice pode não ser eficiente em memória ou GC, dependendo da freqüência da operação, caso em que a iteração, conforme explicado na resposta aceita, seria mais ideal.
TheDarkIn1978
1
@ TheDarkIn1978 Estou ciente de que há uma troca entre a elegância do código e o desempenho do aplicativo 👍🏻
Abdennour TOUMI
38

ES — Shorter

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

O operador spread é um atalho para isso

philipp
fonte
É um operador interessante.
Todos os trabalhadores são essenciais,
Qual é a diferença entre e.parentElement.childNodese e.parentNode.children?
mesqueeb
4
childNodesinclui nós de texto também
philipp
Com o Type 'NodeListOf<ChildNode>' must have a '[Symbol.iterator]()' method that returns an iterator.ts(2488)
texto datilografado,
9

Adicionando um (prefixado para segurança) element.getParentIndex ():

Element.prototype.PREFIXgetParentIndex = function() {
  return Array.prototype.indexOf.call(this.parentNode.children, this);
}
mikemaccana
fonte
1
Há uma razão para o sofrimento do desenvolvimento da web: devs com prefixo saliente. Por que não apenas fazer if (!Element.prototype.getParentIndex) Element.prototype.getParentIndex = function(){ /* code here */ }? De qualquer forma, se isso for implementado no padrão no futuro, provavelmente será implementado como um getter element.parentIndex. Então, eu diria que a melhor abordagem seriaif(!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 o futuro getParentIndex()pode ter uma assinatura diferente da sua implementação.
mikemaccana
7

Eu suponho que dado um elemento onde todos os seus filhos são ordenados no documento sequencialmente, a maneira mais rápida deveria ser fazer uma pesquisa binária, comparando as posições dos elementos no documento. No entanto, conforme apresentado na conclusão, a hipótese é rejeitada. Quanto mais elementos você tiver, maior será o potencial de desempenho. Por exemplo, se você tivesse 256 elementos, então (idealmente) você só precisaria verificar apenas 16 deles! Para 65536, apenas 256! O desempenho chega à potência de 2! Veja mais números / estatísticas. Visite a 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);

Então, a maneira de usá-lo é obtendo a propriedade 'parentIndex' de qualquer elemento. Por exemplo, verifique a seguinte demonstração.

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

Limitações

  • Esta implementação da solução não funcionará no IE8 e versões anteriores.

Pesquisa linear VS binária em 200 mil elementos (pode travar alguns navegadores móveis, CUIDADO!):

  • Neste teste, veremos quanto tempo leva para uma pesquisa linear encontrar o elemento do meio VS uma pesquisa binária. Por que o elemento do meio? Porque está no local médio de todos os outros locais, então ele representa melhor todos os locais possíveis.

Pesquisa Binária

Pesquisa Linear para trás (`lastIndexOf`)

Pesquisa Linear para a frente (`indexOf`)

Pesquisa de contador PreviousElementSibling

Conta o número de PreviousElementSiblings para obter o parentIndex.

Sem Pesquisa

Para fazer o benchmarking, qual seria o resultado do teste se o navegador otimizasse a pesquisa.

The Conculsion

No entanto, depois de ver os resultados no Chrome, os resultados são o oposto do que era esperado. A pesquisa linear direta mais burra foi surpreendentemente 187 ms, 3850%, mais rápida do que a pesquisa binária. Evidentemente, o Chrome de alguma forma magicamente superou o console.asserte o otimizou, ou (de forma mais otimista) o Chrome usa internamente o sistema de indexação numérica para o DOM, e esse sistema de indexação interno é exposto por meio de otimizações aplicadas Array.prototype.indexOfquando usado em um HTMLCollectionobjeto.

Jack Giffin
fonte
Eficiente, mas pouco prático.
applemonkey496
3

Use o algoritmo de pesquisa binária para melhorar o desempenho quando o nó tiver irmãos em grande quantidade.

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;
}
cuixiping
fonte
Não funciona. Sinto-me compelido a salientar que a versão do IE e a versão de "outro navegador" calcularão resultados diferentes. A técnica de "outros navegadores" funciona conforme o esperado, obtendo a enésima posição sob o nó pai, porém a técnica do IE "Recupera a posição ordinal do objeto, na ordem de origem, conforme o objeto aparece em toda a coleção do documento" ( msdn.microsoft .com / en-gb / library / ie / ms534635 (v = vs. 85) .aspx ). Por exemplo, consegui 126 usando a técnica "IE" e 4 usando a outra.
Christopher Bull
1

Tive problemas com nós de texto e ele estava mostrando o índice errado. Aqui está a versão para consertar isso.

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

    return position;
}
John quer saber
fonte
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());
     });
bortunac
fonte
1
Você pode explicar por que você estende Element.prototype? As funções parecem úteis, mas não sei o que elas fazem (mesmo que as nomeações sejam óbvias).
A1rPun
@ extend Element.prototype a razão é similaridade ... 4 ex elemen.children, element.parentNode etc ... então da mesma maneira que você endereça element.siblings .... o método de grupo é um pouco complicado causa que eu quero estender um pouco a abordagem de irmão para elelemts pelo mesmo nodeType e com os mesmos atributos, mesmo não tendo o mesmo ancestral
bortunac
Eu sei o que é extensão de protótipo, mas gosto de saber como seu código é usado. el.group.value()??. Meu primeiro comentário é para melhorar a qualidade da sua resposta.
A1rPun
os métodos group and siblings retornam Array com elementos dom fundados .. .... obrigado pelo seu comentário e pelo motivo do comentário
bortunac
Muito elegante, mas também muito 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>
ofir_aghai
fonte
Você não precisa do jQuery aqui.
Vitaly Zdanevich de