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 arrays, isso é bom. Mas para grandes arrays, 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 arrayao 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.