Esta é uma postagem simples e rápida sobre técnicas de JavaScript. Abordaremos diferentes métodos para combinar/fundir duas matrizes JS e os prós e contras de cada abordagem.
Vamos começar com o cenário:
var a = [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]; var b = [ "foo", "bar", "baz", "bam", "bun", "fun" ];
A simples concatenação de a
e b
seria, obviamente, o senhor:
[ 1, 2, 3, 4, 5, 6, 7, 8, 9, "foo", "bar", "baz", "bam" "bun", "fun" ]
concat(..)
A abordagem mais comum é:
var c = a.concat( b ); a; // [1,2,3,4,5,6,7,8,9] b; // ["foo","bar","baz","bam","bun","fun"] c; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]
Como o senhor pode ver, c
é um novo array
que representa a combinação dos dois a
e b
matrizes, deixando o a
e b
intocado. Simples, não é?
E se a
tiver 10.000 itens, e o b
é de 10.000 itens? c
é agora de 20.000 itens, o que constitui basicamente o dobro do uso de memória do a
e do b
.
“Sem problemas!”, diz o senhor. É só cancelar a configuração do a
e b
portanto, eles são coletados pelo lixo, certo? Problema resolvido!
a = b = null; // `a` and `b` can go away now
Não. Por apenas alguns pequenos array
s, isso é bom. Mas para grandes array
s, ou repetir esse processo regularmente várias vezes, ou trabalhar em ambientes com memória limitada, isso deixa muito a desejar.
Inserção em loop
OK, vamos apenas acrescentar um array
ao conteúdo do outro, usando Array#push(..)
:
// `b` onto `a` for (var i=0; i < b.length; i++) { a.push( b[i] ); } a; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"] b = null;
Agora, a
tem o resultado tanto do original quanto do a
mais o conteúdo do b
.
Parece que é melhor para a memória.
Mas e se o senhor a
fosse pequeno e o b
era comparativamente muito grande? Por motivos de memória e velocidade, o senhor provavelmente gostaria de usar o menor a
para a frente do b
em vez do mais longo b
no final do a
. Não há problema, basta substituir o push(..)
por unshift(..)
e faça um loop na direção oposta:
// `a` into `b`: for (var i=a.length-1; i >= 0; i--) { b.unshift( a[i] ); } b; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"] a = null;
Truques funcionais
UnparaFelizmente, for
os loops são feios e mais difíceis de manter. Podemos fazer algo melhor?
Aqui está nossa primeira tentativa, usando Array#reduce
:
// `b` onto `a`: a = b.reduce( function(coll,item){ coll.push( item ); return coll; }, a ); a; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"] // or `a` into `b`: b = a.reduceRight( function(coll,item){ coll.unshift( item ); return coll; }, b ); b; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]
Array#reduce(..)
e Array#reduceRight(..)
são bons, mas são um pouco desajeitados. ES6 =>
arrow-functions as reduzirão um pouco, mas ainda será necessária uma chamada de função por item, o que é lamentável.
E quanto a:
// `b` onto `a`: a.push.apply( a, b ); a; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"] // or `a` into `b`: b.unshift.apply( b, a ); b; // [1,2,3,4,5,6,7,8,9,"foo","bar","baz","bam","bun","fun"]
Isso é muito mais agradável, certo? Especialmente porque o unshift(..)
aqui não precisa se preocupar com a ordem inversa, como nas tentativas anteriores. O operador de propagação do ES6 será ainda melhor: a.push( ...b )
ou b.unshift( ...a )
.
Mas as coisas não são tão cor-de-rosa quanto parecem. Em ambos os casos, a passagem do a
ou b
para apply(..)
(ou através do ...
spread operator) significa que a matriz está sendo distribuída como argumentos para a função.
O primeiro grande problema é que estamos efetivamente dobrando o tamanho (temporariamente, é claro!) da coisa que está sendo anexada, essencialmente copiando seu conteúdo para a pilha para a chamada de função. Além disso, diferentes mecanismos JS têm diferentes limitações dependentes da implementação para o número de argumentos que podem ser passados.
Portanto, se o array
que está sendo adicionado tiver um milhão de itens, é quase certo que o senhor excederá o tamanho da pilha permitida para esse push(..)
ou unshift(..)
chamada. Ugh. Isso funcionará muito bem para alguns milhares de elementos, mas o senhor precisa ter cuidado para não exceder um limite razoavelmente seguro.
Nota: O senhor pode tentar a mesma coisa com o splice(..)
, mas o senhor terá as mesmas conclusões que com push(..)
/ unshift(..)
.
Uma opção seria usar essa abordagem, mas agrupar os segmentos no tamanho máximo seguro:
function combineInto(a,b) { var len = a.length; for (var i=0; i < len; i=i+5000) { b.unshift.apply( b, a.slice( i, i+5000 ) ); } }
Espere, estamos retrocedendo em termos de legibilidade (e talvez até de desempenho!). Vamos parar antes de perdermos todos os ganhos que tivemos até agora.
Resumo
Array#concat(..)
é a abordagem comprovada e verdadeira para combinar dois (ou mais!) arrays. Mas o perigo oculto é que o senhor está criando um novo array em vez de modificar um dos existentes.
Há opções que modificam no local, mas elas têm várias desvantagens.
Considerando os vários prós e contras, talvez a melhor de todas as opções (incluindo outras não mostradas) seja o reduce(..)
e o reduceRight(..)
.
Seja qual for sua escolha, provavelmente é uma boa ideia pensar criticamente sobre sua estratégia de mesclagem de array, em vez de tomá-la como garantida.
Sobre Kyle Simpson
Kyle Simpson é um engenheiro de software orientado para a Web, amplamente aclamado por sua série de livros “You Don’t Know JS” e por quase 1 milhão de horas vistas de seus cursos on-line. O superpoder de Kyle é fazer perguntas melhores, e ele acredita profundamente no uso máximo das ferramentas minimamente necessárias para qualquer tarefa. Como um “tecnólogo centrado no ser humano”, ele é apaixonado por unir humanos e tecnologia, desenvolvendo organizações de engenharia para resolver os problemas certos, de maneiras mais simples. Kyle sempre lutará pelas pessoas por trás dos pixels.