O paradigma map/reduce (e seus amigos filter, each,
flatten etc) oferece uma maneira geral de manipular listas e streams.
Isto se encaixa particularmente bem no trabalho web – onde gastamos a maior
parte de nossos dias brincando com listas de elementos DOM. Versões recentes do JavaScript nos deram ferramentas para
fazer esse trabalho nativamente, mas antes disso tínhamos que fazer tudo
sozinhos, ou usar uma biblioteca. O jQuery tem apresentado alguns recursos parecidos,
desde sempre, então hoje faremos um pouco de “comparar e contrastar” na função map: jQuery vs jQuery vs
JavaScript!
O Map itera sobre uma coleção, e aplica a
função a cada elemento – retornando uma lista nova, normalmente modificada.
Nossos testes irão analisar o “inline” map do jQuery com seu mapa global
$.map, e a função d J native map do JavaScript. O objetivo é ver como
cada implementação varia em termos de parâmetros e valores do this que temos a cada
iteração, e como eles lidam com arrays estranhas.
Nossa tarefa inicial será
transformar uma lista de tags <li> em valores numéricos contidos em cada
elementos. Portanto, o input será:
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
O output exigido é uma simples array do JavaScript:
[1, 2, 3]
O jQuery gosta de deixar as
coisas de seu jeito, então alguns dos outputs (e inputs) estarão encapsulados
dentro da função jQuery (p.e. jQuery(1, 2, 3) ao invés de [1, 2, 3]). O logger do Chrome irá mostrar
ambos os valores como [1, 2, 3] – mas se você tentar .revert um objeto jQuery,
ele iria morrer – então você precisa de chegar até o array da camada de baixo
de um objeto jQuery e então usar o método get: jQuery(1, 2, 3).get().
O código para os testes
está no GitHub. Eu abstrai os testes um pouco para garantir sua legibilidade,
mas listarei uma “versão longa” aqui para que ela faça mais sentido quando fora
do contexto. Cada método irá imprimir a coleção (“Input”), o valor this e seus argumentos (“args”),
para cada iteração de item, e o resultado (“Output”).
Ok, vamos fazer alguns
testes:
Ex 1a: inline jQuery
map
$("#list li").map(function(){
return parseInt($(this).text(), 10);
});
Input: jQuery(li, li, li)
this: <li> args: [0, li]
this: <li> args: [1, li]
this: <li> args: [2, li]
Output: jQuery(1, 2, 3)
Ex 1b: global jQuery map
$.map($("#list li"), function(el){
return parseInt($(el).text(), 10);
});
Input: jQuery(li, li, li)
this: DOMWindow args: [li, 0, undefined]
this: DOMWindow args: [li, 1, undefined]
this: DOMWindow args: [li, 2, undefined]
Output: [1, 2, 3]
Existem algumas diferenças
notáveis entre os dois métodos jQuery. A versão inline gera o escopo para
a lista, então o this
é o elemento, e o primeiro argumento a executar o callback é o index do
elemento. O segundo é o mesmo do this
(por algum motivo).
A versão global gera o escopo
para a janela do documento. Para conseguir o elemento, você precisa procurar
pelo primeiro parâmetro. O índex é o segundo, e teremos uma referencia
não-documentada como undefined
como a terceira. (eu realmente deveria ter procurado melhor no código fonte do jQuery
para ver o que estava acontecendo lá). O map global também retorna um array
real do javaScript – não um array do tipo jQuery.
Ex 1c: JavaScript map over array
$("#list li").get().map(function(el){
return parseInt($(el).text(), 10);
});
Input: [li, li, li]
this: DOMWindow args: [li, 0, [li, li, li]]
this: DOMWindow args: [li, 1, [li, li, li]]
this: DOMWindow args: [li, 2, [li, li, li]]
Output: [1, 2, 3]
O map nativo do JavaScript nos
dá um output que parece como o map global do jQuery – exceto pelo fato que
temos algo um pouco mais útil no terceiro parâmetro: a coleção original. A
coleção original nesse caso é o valor .get() da seleção do jQuery – ou seja,
um array real. Mas ele funciona em uma coleção jQuery também, se a aplicarmos
como no exemplo a seguir:
Ex 1d: JavaScript map
over jQuery collection
Array.prototype.map.call($("#list li"), function(el){
return parseInt($(el).text(), 10);
});
Input: jQuery(li, li, li)
this: DOMWindow args: [li, 0, jQuery(li, li, li)]
this: DOMWindow args: [li, 1, jQuery(li, li, li)]
this: DOMWindow args: [li, 2, jQuery(li, li, li)]
Output: [1, 2, 3]
Isso foi apenas para mostrar
que o output era o mesmo com o input jQuery. O map nativo também permite que
você especifique um parâmetro opcional depois da função callback para gerar o
escopo do map também – ao invés do objeto DOMWindow.
Existem algumas coisas em
comum com cada implementação: todas elas tem um modo de acessar os elementos e
o índex dos elementos (zero offset), e nenhum deles transformam a coleção
original. Não é difícil de compreender as diferenças e adaptar seu estilo – mas
nem tudo são rosas quando você sai do mundo jQuery…
Santos arrays, Batman!
Todas as três
implementações do map trabalham sobre as boas e velhas arrays regulares. No entanto, as boas e velhas arrays
no JavaScript têm a tendência de não serem regulares: podemos ter valores
nulos, e “buracos” que podem acabar com nossos bravos iteradores.
var holey = [ "1", "2", null, ["4a", "4b"], , "6"];
Output: ["1", "2", null, Array[2], undefined, "6"]
Aqui temos um array que é
bastante estranho. Ele tem strings, arrays, buracos e valores nulos. Primeiramente,
vamos ver como nosso map faz o índex desse monstrinho:
Ex 2a: jQuery global
map-to-index
$.map(holey, function(el, index){
return index;
});
Output: [0, 1, 2, 3, 4, 5]
Ex 2b: JavaScript map-to-index
holey.map(function(el, index){
return index;
});
Output: [0, 1, 2, 3, undefined, 5]
Ok, seja cuidadoso aqui – nosso “buraco” não foi processado pelo nosso map nativo, então não temos nenhum
valor para o índex, o que resulta em um grande undefined no meio. É melhor ter isto em
mente.
Finalmente, vamos ver o que
acontece se usarmos os elementos map neles mesmos:
Ex 2c: JavaScript
map-to-element
holey.map(function(el){
return el;
});
Output: ["1", "2", null, ["4a", "4b"], undefined, "6"]
Ex 2d: jQuery global map-to-element
$.map(holey, function(el){
return el;
});
Output: ["1", "2", "4a", "4b", "6"]
Errrgh… Um, uau? O JavaScript
nativo age como esperado, mas existe algo peculiar acontecendo no mundo do jQuery.
Na verdade, se você consultar os arquivos, você verá que isso não é uma falha,
mas um recurso. Valores nulos são retirados do map resultante, e qualquer array
é comprimida em um nível (na verdade, se você precisar combater isso, você pode
retornar o elemento encapsulado em uma array extra).
Este é um recurso meio doido
– o map é na verdade um flatmap e um tipo de filtro no jQuery. Então tenha
certeza que você sabe o que você está fazendo antes de ir brincando com as
arrays do JavaScript!
Ok. É isso por hoje. As
coisas ficarão ainda mais malucas se você deletar itens durante um loop, mas se
você estiver fazendo isso você está sozinho. Em um próximo artigo, veremos alguns
métodos map/reduce/filter/etc em ação, e veremos como é importante ter
dissecado o velho map. Te vejo lá.
?
Texto original disponível em http://www.mrspeaker.net/2011/04/27/reducing-map/