Como converter uma lista de nós DOM em uma matriz em Javascript?

96

Eu tenho uma função Javascript que aceita uma lista de nós HTML, mas espera um array Javascript (executa alguns métodos Array nisso) e quero alimentá-lo com a saída Document.getElementsByTagNameque retorna uma lista de nós DOM.

Inicialmente pensei em usar algo simples como:

Array.prototype.slice.call(list,0)

E isso funciona bem em todos os navegadores, exceto, é claro, o Internet Explorer que retorna o erro "objeto JScript esperado", pois aparentemente a lista de nós DOM retornada por Document.getElement*métodos não é um objeto JScript o suficiente para ser o alvo de uma chamada de função.

Advertências: não me importo de escrever código específico do Internet Explorer, mas não tenho permissão para usar nenhuma biblioteca Javascript, como JQuery, porque estou escrevendo um widget para ser incorporado em um site de terceiros e não consigo carregar bibliotecas externas que criará conflito para os clientes.

Meu último esforço é iterar a lista de nós DOM e eu mesmo criar um array, mas existe uma maneira melhor de fazer isso?

Guss
fonte
Melhor ainda, crie uma função para converter da lista de nós DOM, mas essa seria realmente a minha solução, acho que você acertou.
Kristoffer Sall-Storgaard
> for (i = 0; i & lt; x.length; i ++) Por que obter o comprimento da NodeList a cada iteração? Não é apenas uma perda de tempo, mas como os NodeLists são coleções ativas, se algo no corpo do loop mudar sua duração, você poderá fazer um loop infinito ou atingir um índice fora dos limites. O último é o pior que pode acontecer se você atribuir o comprimento a uma variável, e um erro é muito melhor do que um loop infinito.
Esta é uma questão muito antiga, mas o jQuery foi construído com o método .noConflict especificamente para não causar conflito com outras bibliotecas (mesmo com ela mesma), o que significa que várias versões do jQuery podem ser carregadas em uma página. Dito isso, é melhor evitar usar / carregar uma biblioteca, a menos que seja absolutamente necessário.
vol7ron
@ vol7ron: avance para 2016, e todos ainda estão preocupados com o tamanho que as bibliotecas javascript adicionam à página. Concedido, JQuery minificado e gzipado tem 30 KB, ainda é 30 KB demais apenas para transformar uma lista de nós :-)
Guss

Respostas:

64

NodeLists são objetos de host , o uso do Array.prototype.slicemétodo em objetos de host não tem garantia de funcionamento, a Especificação ECMAScript afirma:

Se a função de fatia pode ser aplicada com sucesso a um objeto hospedeiro depende da implementação.

Eu recomendaria que você fizesse uma função simples para iterar NodeListe adicionar cada elemento existente a uma matriz:

function toArray(obj) {
  var array = [];
  // iterate backwards ensuring that length is an UInt32
  for (var i = obj.length >>> 0; i--;) { 
    array[i] = obj[i];
  }
  return array;
}

ATUALIZAR:

Como outras respostas sugerem, agora você pode usar em ambientes modernos a sintaxe de propagação ou o Array.frommétodo:

const array = [ ...nodeList ] // or Array.from(nodeList)

Mas pensando bem, acho que o caso de uso mais comum para converter um NodeList em um Array é iterar sobre ele, e agora o NodeList.prototypeobjeto tem o forEachmétodo nativamente , então se você estiver em um ambiente moderno, você pode usá-lo diretamente, ou ter um pollyfill.

CMS
fonte
2
Isso é criar um array com a ordem original da lista invertida, o que não acho que seja o que o OP deseja. Você quis dizer fazer em array[i] = obj[i]vez de array.push(obj[i])?
Tim Down
@Tim, certo, eu já tinha feito isso antes, mas editei ontem à noite sem perceber (3h hora local :), obrigado !.
CMS
9
Em que circunstâncias seria obj.lengthalgo diferente de um valor inteiro?
Peter
1
Não acredito que seja tão complicado. Feio. Essa é uma necessidade muito comum na programação Web / JS. Um novo método para o próximo lançamento da linguagem?
Andrew Koper
1
@AlbertoPerez, de nada !. Saludos hasta Madrid!
CMS
126

Em es6, você pode apenas usar o seguinte:

  • Operador Spread

     var elements = [... nodelist]
  • Usando Array.from

     var elements = Array.from(nodelist)

mais referências em https://developer.mozilla.org/en-US/docs/Web/API/NodeList

camiloazula
fonte
4
tão fácil com Array.from(): D
Josan Iracheta
4
caso alguém esteja usando essa abordagem com Typescript (para ES5), só Array.fromfunciona, pois o TS transpila para nodelist.slice- o que não é suportado.
Peter Albert
Eu respondi a mesma coisa um ano antes de você e você me passou nos votos? Não consigo explicar isso ..
vsync
3
@vsync, sua resposta não mencionaArray.from
ESR
@EdmundReed - e daí? como isso o justifica. é mais longo para escrever, então na situação real nunca vai conseguir ser usado, apenas spreadé usado.
vsync
16

Usando spread (ES2015) , é tão fácil quanto:[...document.querySelectorAll('p')]

(opcional: use Babel para transpilar o código ES6 acima para a sintaxe ES5)


Experimente no console do seu navegador e veja a mágica:

for( links of [...document.links] )
  console.log(links);
vsync
fonte
Pelo menos o Chrome mais recente, 44, eu recebo isto: Uncaught TypeError: document.querySelectorAll não é uma função (...)
Nick
@OmidHezaveh - Como eu disse, esse é o código ES6. Não sei se o Chrome 44 suporta ES6 e, em caso afirmativo, com que cobertura. É um navegador com quase um ano de idade e obviamente você teria que executar este código em um navegador que suporte a propagação ES6.
vsync
Ou transpile-o para ES5 antes da execução
HelloWorld
8

Use este truque simples

<Your array> = [].map.call(<Your dom array>, function(el) {
    return el;
})
Gena Shumilkin
fonte
Você pode explicar por que você acha que isso tem mais chance de sucesso do que usar Array.prototype.slice(ou [].slicecomo você colocou)? Como observação, gostaria de comentar que o erro específico do IE que documentei no Q acontece no IE 8 ou inferior, onde mapnão está implementado de qualquer maneira. No IE 9 ("modo padrão") ou superior, ambos slicee mapbem - sucedidos da mesma maneira.
Guss
6

Embora não seja realmente um shim adequado, uma vez que não há especificações exigindo trabalhar com elementos DOM, fiz um para permitir que você use slice()desta maneira: https://gist.github.com/brettz9/6093105

ATUALIZAÇÃO : quando eu levantei isso com o editor da especificação DOM4 (perguntando se eles poderiam adicionar suas próprias restrições aos objetos de host (para que a especificação exigisse que os implementadores convertessem adequadamente esses objetos quando usados ​​com métodos de array) além da especificação ECMAScript que tinha permitido para a independência de implementação), ele respondeu que "Objetos de host são mais ou menos obsoletos por ES6 / IDL." Vejo por http://www.w3.org/TR/WebIDL/#es-array que as especificações podem usar este IDL para definir "objetos de matriz de plataforma", mas http://www.w3.org/TR/domcore/ não parece estar usando o novo IDL para HTMLCollection(embora pareça que pode estar Element.attributesusando, embora apenas afirme explicitamente que está usando WebIDL para DOMString e DOMTimeStamp). vejo sim[ArrayClass](que herda de Array.prototype) é usado para NodeList(e NamedNodeMapagora está obsoleto em favor do único item que ainda o estaria usando Element.attributes). Em qualquer caso, parece que se tornará um padrão. O ES6 Array.fromtambém pode ser mais conveniente para tais conversões do que ter que especificar Array.prototype.slicee mais claro do que semanticamente [].slice()(e a forma mais curta, Array.slice()(um "array genérico"), até onde eu sei, não se tornou um comportamento padrão).

Brett Zamir
fonte
Atualizei para indicar que as especificações podem estar se movendo na direção de exigir esse comportamento.
Brett Zamir de
5

Hoje, em 2018, poderíamos usar o ECMAScript 2015 (6ª edição) ou ES6, mas nem todos os navegadores conseguem entendê-lo (por exemplo, o IE não entende tudo). Se você quiser, pode usar o ES6 da seguinte maneira: var array = [... NodeList];( como operador de spread ) ou var array = Array.from(NodeList);.

Em outro caso (se você não pode usar ES6), você pode usar o caminho mais curto para converter um NodeListem Array:

var array = [].slice.call(NodeList, 0);.

Por exemplo:

var nodeList = document.querySelectorAll('input');
//we use "{}.toString.call(Object).slice(8, -1)" to find the class name of object
console.log({}.toString.call(nodeList).slice(8, -1)); //NodeList

var array = [].slice.call(nodeList, 0);
console.log({}.toString.call(array).slice(8, -1)); //Array

var result = array.filter(function(item){return item.value.length > 5});

for(var i in result)
  console.log(result[i].value); //credit, confidence
<input type="text" value="trust"><br><br>
<input type="text" value="credit"><br><br>
<input type="text" value="confidence">

Mas se você deseja iterar DOMfacilmente na lista de nós, não é necessário converter a NodeListem um Array. É possível fazer um loop sobre os itens em um NodeListusando:

var nodeList = document.querySelectorAll('input');
// Calling nodeList.item(i) isn't necessary in JavaScript
for(var i = 0; i < nodeList.length; i++)
    console.log(nodeList[i].value); //trust, credit, confidence
<input type="text" value="trust"><br><br>
<input type="text" value="credit"><br><br>
<input type="text" value="confidence">

Não fique tentado a usar for...inou for each...inenumerar os itens da lista, pois isso também enumerará o comprimento e as propriedades do item do NodeListe causará erros se o seu script assumir que ele só precisa lidar com objetos de elemento. Além disso, for..innão é garantido visitar as propriedades em qualquer ordem específica. for...ofos loops farão um loop nos objetos NodeList corretamente.

Veja também:

Bharata
fonte
3
var arr = new Array();
var x= ... get your nodes;

for (i=0;i<x.length;i++)
{
  if (x.item(i).nodeType==1)
  {
    arr.push(x.item(i));
  }
}

Isso deve funcionar, cruzar o navegador e obter todos os nós de "elemento".

Strelok
fonte
1
Esta é basicamente a mesma que a resposta de @CMS, exceto que assume que eu quero apenas nós de elemento - o que eu não quero.
Guss