Por que document.querySelectorAll retorna um StaticNodeList em vez de um Array real?

103

Isso me incomoda que eu não consigo fazer apenas document.querySelectorAll(...).map(...)no Firefox 3.6, e ainda não consigo encontrar uma resposta, então pensei em fazer uma postagem cruzada no SO a pergunta deste blog:

http://blowery.org/2008/08/29/yay-for-queryselectorall-boo-for-staticnodelist/

Alguém sabe de um motivo técnico pelo qual você não consegue um Array? Ou por um StaticNodeList não herda de uma matriz de tal forma a que você poderia usar map, concat, etc?

(Aliás, se for apenas uma função que você deseja, você pode fazer algo como NodeList.prototype.map = Array.prototype.map;... mas, novamente, por que essa funcionalidade (intencionalmente?) Está bloqueada em primeiro lugar?)

Kev
fonte
3
Na verdade também getElementsByTagName não retorna um Array, mas uma coleção, e se você quiser usá-lo como um Array (com métodos como concat etc.), você deve converter tal coleção em um Array fazendo um loop e copiar cada elemento do coleção em um Array. Ninguém nunca reclamou disso.
Marco Demaio

Respostas:

81

Acredito que seja uma decisão filosófica do W3C. O design do W3C DOM [spec] é bastante ortogonal ao design do JavaScript, pois o DOM deve ser neutro em relação à plataforma e à linguagem.

Decisões como " getElementsByFoo()retorna um ordenado NodeList" ou " querySelectorAll()retorna um StaticNodeList" são muito intencionais, de modo que as implementações não precisam se preocupar em alinhar sua estrutura de dados retornada com base em implementações dependentes de linguagem (como .mapestar disponível em Arrays em JavaScript e Ruby, mas não em listas em C #).

O objetivo do W3C é baixo: eles dirão que a NodeListdeve conter uma propriedade somente leitura .lengthdo tipo unsigned long porque acreditam que cada implementação pode pelo menos suportar isso , mas não dirão explicitamente que o []operador de índice deve ser sobrecarregado para suportar a obtenção de elementos posicionais, porque eles não querem bloquear uma pequena linguagem pobre que surge e que deseja implementar, getElementsByFoo()mas não pode suportar a sobrecarga do operador. É uma filosofia predominante presente em grande parte das especificações.

John Resig expressou uma opção semelhante à sua, à qual acrescenta :

Meu argumento não é tanto que NodeIteratornão seja muito semelhante ao DOM, mas sim que não é muito semelhante ao JavaScript. Não tira proveito dos recursos presentes na linguagem JavaScript e os usa com o melhor de sua capacidade ...

Eu empatia um pouco. Se o DOM fosse escrito especificamente com recursos JavaScript em mente, seria muito menos complicado e mais intuitivo de usar. Ao mesmo tempo, eu entendo as decisões de design do W3C.

Crescent Fresh
fonte
Obrigado, isso me ajuda a entender a situação.
Kev
@Kev: Eu vi seu comentário na página do artigo do blog questionando como você faria para converter o StaticNodeListpara um array. Eu endossaria a resposta de @ mck89 como o caminho a seguir para converter um NodeList/ StaticNodeListpara um Array nativo, mas isso irá falhar no IE (óbvio 8) com um erro de JScript, uma vez que esses objetos são hospedados / "especiais".
Crescent Fresh,
Verdade, é por isso que eu votei a favor dele. Porém, outra pessoa cancelou meu +1. O que você quer dizer com hospedado / especial?
Kev
1
@Kev: variáveis ​​hospedadas são quaisquer variáveis ​​fornecidas pelo ambiente "host" (por exemplo, um navegador da web). Por exemplo document, windowetc. O IE geralmente implementa esses "especialmente" (por exemplo, como objetos COM) que às vezes não se conformam ao uso normal, de maneiras pequenas e sutis, como Array.prototype.slice.callbombardeio quando dado a StaticNodeList;)
Crescent Fresh
200

Você pode usar o operador de propagação ES2015 (ES6) :

[...document.querySelectorAll('div')]

irá converter StaticNodeList em Array de itens.

Aqui está um exemplo de como usá-lo.

[...document.querySelectorAll('div')].map(x => console.log(x.innerHTML))
<div>Text 1</div>
<div>Text 2</div>

Vlad Bezden
fonte
24
Outra maneira é usar Array.from () :Array.from(document.querySelectorAll('div')).map(x => console.log(x.innerHTML))
Michael Berdyshev
42

Não sei por que ele retorna uma lista de nós em vez de uma matriz, talvez porque, como getElementsByTagName, ele atualizará o resultado quando você atualizar o DOM. De qualquer forma, um método muito simples para transformar esse resultado em uma matriz simples é:

Array.prototype.slice.call(document.querySelectorAll(...));

e então você pode fazer:

Array.prototype.slice.call(document.querySelectorAll(...)).map(...);
mck89
fonte
3
Na verdade, ele não atualiza o resultado quando você atualiza o DOM - portanto, 'estático'. Você deve chamar manualmente o qSA novamente para atualizar o resultado. 1 para a slicelinha embora.
Kev,
1
Sim, como Kev disse: o conjunto de resultados qSA é estático, o conjunto de resultados getElementsByTagName () é dinâmico.
joonas.fi
O IE8 só oferece suporte a querySelectorAll () no modo padrão
mbokil
13

Só para acrescentar ao que Crescent disse,

se for apenas uma função desejada, você pode fazer algo como NodeList.prototype.map = Array.prototype.map

Não faça isso! Não é garantido que funcione.

Nenhum padrão JavaScript ou DOM / BOM especifica que a NodeListfunção construtora existe mesmo como uma windowpropriedade / global , ou que o NodeListretornado por querySelectorAllherdará dela, ou que seu protótipo é gravável, ou que a função Array.prototype.maprealmente funcionará em um NodeList.

Um NodeList pode ser um 'objeto host' (e é um, no IE e em alguns navegadores mais antigos). Os Arraymétodos são definidos como tendo permissão para operar em qualquer 'objeto nativo' de JavaScript que exponha numéricos e lengthpropriedades, mas eles não são obrigados a funcionar em objetos de host (e no IE, eles não funcionam).

É irritante que você não obtenha todos os métodos de array nas listas DOM (todos eles, não apenas StaticNodeList), mas não há uma maneira confiável de contornar isso. Você terá que converter cada lista DOM que retornar em um Array manualmente:

Array.fromList= function(list) {
    var array= new Array(list.length);
    for (var i= 0, n= list.length; i<n; i++)
        array[i]= list[i];
    return array;
};

Array.fromList(element.childNodes).forEach(function() {
    ...
});
bobince
fonte
1
Droga, eu não pensei nisso. Obrigado!
Kev
Eu concordo +1. Apenas um comentário, acho que fazer "var array = []" em vez de "var array = new Array (list.length)" tornaria o código ainda mais curto. Mas estou interessado se você sabe que pode haver um problema em fazer isso.
Marco Demaio de
@MarcoDemaio: Não tem problema. new Array(n)apenas dá ao terp JS uma dica de quanto tempo o array vai acabar. Isso poderia permitir que ele alocasse essa quantidade de espaço com antecedência, o que poderia potencialmente resultar em um aumento de velocidade, já que algumas realocações de memória poderiam ser evitadas conforme o array cresce. Não sei se isso realmente ajuda em navegadores modernos ... Eu suspeito que não seja mensurável.
bobince de
2
Agora está implementado em Array.from ()
Michael Berdyshev
2

Eu acho que você pode simplesmente seguir

Array.prototype.map.call(document.querySelectorAll(...), function(...){...});

Funciona perfeito para mim

Max Leps
fonte
0

Esta é uma opção que eu gostaria de acrescentar ao leque de outras possibilidades sugeridas por outros aqui. Destina-se apenas à diversão intelectual e não é aconselhável .


Só por diversão , aqui está uma maneira de "forçar" querySelectorAlla se ajoelhar e se curvar diante de você:

Element.prototype.querySelectorAll = (function(QSA){
    return function(){
        return [...QSA.call(this, arguments[0])]
    }
})(Element.prototype.querySelectorAll);

Agora é bom passar por cima dessa função, mostrando quem manda. Agora eu não sei o que é melhor, criar um novo wrapper de função nomeada e, em seguida, fazer com que todo o seu código use aquele nome estranho (quase no estilo jQuery) ou sobrescreva a função como acima uma vez para que o resto do seu código ainda seja capaz para usar o nome do método DOM original querySelectorAll.

Eu não recomendaria isso de forma alguma, a menos que você honestamente não dê a [você sabe o quê].

vsync
fonte