Artigo original: Hacks for Creating JavaScript Arrays

Escrito por: Glad Chinda

Dicas interessantes para criar e clonar arrays em JavaScript.

Um aspecto muito importante de cada linguagem de programação são os tipos e estruturas de dados disponíveis na linguagem. A maioria das linguagens de programação fornece tipos de dados para representar e trabalhar com dados complexos. Se você já trabalhou com linguagens como Python ou Ruby, deve ter visto tipos de dados como listas, sets, tuplas, hashes, dicionários e assim por diante.

No JavaScript, não há muitos tipos de dados complexos – você simplesmente tem arrays e objetos. Entretanto, no ES6, alguns tipos de dados e estruturas foram adicionados à linguagem, como símbolos, sets e mapas.

Arrays em JavaScript são objetos de alto nível, semelhantes a listas, com propriedades de comprimento (length) e propriedades de números inteiros ,como índices.

Neste artigo, compartilho alguns hacks para a criação de arrays ou para a clonagem de arrays existentes em JavaScript.

Criando arrays: o construtor array

O método mais popular para criar arrays é usar a sintaxe literal do array, que é muito simples. Entretanto, quando se deseja criar arrays dinamicamente, a sintaxe literal do array pode não ser sempre o melhor método. Um método alternativo é a utilização do construtor Array.

Aqui está um trecho de código simples mostrando o uso do construtor Array:

// MAIS DE UM ARGUMENTO:
// Cria uma array com os argumentos como itens.
// O comprimento do array é definido pelo número de argumentos.

var array1 = new Array(1, 2, 3);

console.log(array1); // [1, 2, 3]
console.log(array1.length); // 3


// APENAS UM (NÚMERO) ARGUMENTO:
// Cria um array com comprimento definido pelo número.
// O número deve ser um número inteiro positivo. Caso contrário, será lançado um erro.
// Observe que o array não tem chaves de propriedade além do comprimento.

var array2 = new Array(3);

console.log(array2); // Array(3) {length: 3}
console.log(array2.length); // 3


// APENAS UM ARGUMENTO (SEM NÚMERO):
// Cria um novo array com o argumento como único item.
// O comprimento do array está definido em 1.

var array3 = new Array("3");

console.log(array3); // ["3"]
console.log(array3.length); // 1

No trecho acima, podemos ver que o construtor Array cria arrays de modo diferente, dependendo dos argumentos que recebe.

Novos arrays com comprimento definido

Vamos olhar mais de perto o que acontece ao criar um Array de comprimento determinado. O construtor apenas define a propriedade length do array como o comprimento dado, sem definir as chaves.

HuAR3m0WmxP390Ezk4Ufyam-9vlyDzwNvEzi-1

Pelo trecho acima, você pode se sentir tentado a pensar que cada chave do array foi definida com o valor undefined. A realidade, porém, é que essas chaves nunca foram definidas (elas não existem).

A ilustração a seguir torna isso mais claro:

wtfHBQo1MBofKp-EVs-IB6qqq07cfM18rK8I-1

Isto torna inútil tentar usar qualquer um dos métodos de iteração de array, como map(), filter() ou reduce() para manipulá-lo. Digamos que queremos preencher cada índice no array com o número 5 como um valor. Vamos tentar o seguinte:

1OHTGXHuG93TuWcOpzRVtUNjoB-BFP3Pykq5-1

Podemos ver que map() não funcionou aqui, pois as propriedades do índice não existem no array – existe apenas a propriedade length.

Vejamos diferentes maneiras de consertar essa questão.

1. Usando Array.prototype.fill()

O método fill() preenche todos os elementos de um array desde um índice inicial até um índice final com um valor estático. O índice final não está incluído. Você pode saber mais sobre fill() aqui.

Observe que fill() só funcionará em navegadores com suporte à ES6.

Aqui está uma ilustração simples:

w3CWlvnWqG5VEy6qupnAYvTqECGhPdj3P9Wu-1

Aqui, conseguimos preencher todos os elementos do nosso array criado com 5. Você pode definir qualquer valor estático para diferentes índices do array usando o método fill().

2. Usando Array.from()

O método Array.from() cria uma instância de um Array quando for passado um objeto iterável ou semelhante a um array. Você pode aprender mais sobre o Array.from() aqui.

Observe que Array.from() só funcionará em navegadores com suporte à ES6.

Aqui está uma ilustração simples:

XfZGhDWQWU1VwxliMKIjYgHuwYvkfvPSBkVT-1

Aqui, temos valores undefined estabelecidos de verdade para cada elemento do array usando o Array.from(). Isso significa que agora podemos avançar e usar métodos como .map() e .filter() no array, uma vez que as propriedades de índice agora existem.

Mais uma coisa que vale a pena notar sobre o Array.from() é que ele pode ter um segundo argumento, que é uma função map. Ela será chamada em cada elemento do array. Isto torna redundante chamar .map() após o Array.from().

Aqui está um exemplo simples:

UgaAHFIo4xzuw4cc4bI1iaxaPzGHKkTCbjYK-1

3. Usando o operador spread

O operador spread (...), no ES6, pode ser usado para distribuir os elementos do array, definindo os elementos que faltam como valores undefined. Isto produzirá o mesmo resultado que simplesmente chamar o Array.from() apenas com o array como único argumento.

Aqui está uma ilustração simples do uso do operador spread:

gZrwaPsFq15WkPf2BnuAb2wA54JdIEXx7VNv-1

Você pode usar métodos como .map() e .filter() no array, uma vez que, agora, as propriedades do índice existem.

Usando Array.of()

Assim como vimos ao criar arrays usando o construtor Array ou funções, Array.of() comporta-se de modo muito semelhante. Na verdade, a única diferença entre Array.of() e Array está em como eles lidam com um único argumento inteiro passado para eles.

Enquanto Array.of(5) cria um array com um único elemento, 5, e uma propriedade de comprimento de 1, Array(5) cria uma array vazio com 5 espaços vazios e uma propriedade de comprimento de 5

var array1 = Array.of(5); // [5]
var array2 = Array(5); // Array(5) {length: 5}

Excetuando essa grande diferença, Array.of() comporta-se como o construtor Array. Você pode saber mais sobre o Array.of() aqui.

Observe que Array.of() só funcionará em navegadores com suporte à ES6.

Convertendo para arrays: iteráveis e objetos semelhantes a arrays

Se você escreve funções em JavaScript há tempo suficiente, já deve saber sobre o objeto arguments – que é um objeto semelhante a um array disponível em cada função para manter a lista de argumentos da função recebida. Embora o objeto arguments se pareça muito com um array, ele não tem acesso aos métodos de Array.prototype.

Antes da ES6, você normalmente veria um trecho de código como o seguinte ao tentar converter o objeto arguments em um array:

NaMaYAla-PzcPacVZGr3E03twovKgKTuRVwm-1

Com Array.from() ou com o operador spread, você pode converter convenientemente qualquer objeto semelhante a um array em um array propriamente dito. Portanto, ao invés de fazer isto:

var args = Array.prototype.slice.call(arguments);

você pode fazer qualquer um destes:‌  

// Usando Array.from()
var args = Array.from(arguments);

// Usando o Spread operator
var args = [...arguments];

Isso também se aplica a iteráveis, como mostra a ilustração a seguir:

87TEnXS9-qV7Lak1XBBcEYiQVvh96FqXDJXc-1

Estudo de caso: função range

Como um estudo de caso antes de prosseguirmos, criaremos uma função simples range() para implementar o novo hack de array que acabamos de aprender. A função tem a seguinte assinatura:

range(inicio: numero, fim: numero, passo: numero) => Array<numero>

Aqui está o trecho de código:

/**
 * range()
 *
 * Retorna um array de números entre um número inicial e um número final incrementado
 * sequencialmente por um número fixo (passo), começando com o número inicial ou
 * o número final, dependendo de qual é maior.
 *
 * @param {número} inicio (Necessário: O número inicial.)
 * @param {número} fim (Necessário: O número final. Se o fim for menor que o início,
 *  então o range começa com o fim ao invés do início e decremento em vez de incremento.)
 * @param {número} passo (Opcional: O incremento fixo ou o passo de decremento. Padrão a 1.)
 *
 * @retorna {array} (Um array contendo um intervalo de números.)
 *
 * @lança {TipoErro} (Se qualquer um dos números do início, fim e passo não for um número finito.)
 * @lança {Erro} (Se o passo não for um número positivo.)
 */
function range(inicio, fim, passo = 1) {
  
  // Teste que os 3 primeiros argumentos são números finitos.
  // Usando Array.prototype.every() e Number.isFinite().
  const allNumbers = [inicio, fim, passo].every(Number.isFinite);

  // Lança um erro se algum dos 3 primeiros argumentos não for um número finito.
  if (!allNumbers) {
    throw new TypeError('range() espera apenas números finitos como argumentos.');
  }
  
  // Garante que o passo seja sempre um número positivo.
  if (passo <= 0) {
    throw new Error('Passo deve ser um número maior que 0.');
  }
  
  // Quando o número inicial é maior do que o número final,
  // modifica a etapa para decrementar em vez de incrementar.
  if (inicio > fim) {
    passo = -passo;
  }
  
  // Determina o comprimento do array a ser retornado.
  // O comprimento é incrementado em 1 após Math.floor().
  // Isso garante que o número final seja listado se estiver dentro do intervalo.
  const length = Math.floor(Math.abs((fim - inicio) / passo)) + 1;
  
  // Preencher uma novo array com o intervalo de números
  // usando Array.from() com uma função de mapeamento.
  // Finalmente, retorna o novo array.
  return Array.from(Array(length), (x, indice) => inicio + indice * passo);
  
}

Nesse trecho de código, usamos Array.from() para criar o range (intervalo, em português) do array de comprimento dinâmico e, em seguida, preenchê-lo com números incrementais sequencialmente, fornecendo uma função de mapeamento.

Observe que o trecho de código acima não funcionará para navegadores sem suporte à ES6, exceto se você usar polyfills.

Aqui estão alguns resultados de chamar a função range() definida no trecho de código acima:

zFBQwh8KfkoDDWXcZDl8YnDe7e9jBEsocdCa-1

Você pode obter uma demonstração de código ao vivo executando a seguinte pen no Codepen:

Clonando arrays: o desafio

Em JavaScript, arrays e objetos são tipos de referência. Isto significa que, quando uma variável é atribuída a um array ou objeto, o que é atribuído à variável é uma referência ao local na memória onde o array ou objeto foi armazenado.

Arrays, como qualquer outro objeto em JavaScript, são tipos de referência. Isto significa que os arrays são copiados por referência e não por valor.

O armazenamento de tipos de referência desse modo tem as seguintes consequências:

1. Arrays similares não são iguais.

brYlg3Dp3gVRqGqHGkMBR2XR7eLvWQK8xymc

Aqui, vemos que, embora array1 e array2 contenham aparentemente as mesmas especificações de array, não são iguais. Isso porque a referência a cada um dos arrays aponta para um local diferente na memória.

2. Arrays são copiados por referência e não por valor.

PZF-lU5f4C-OWkNLeF-q4g9T2anP6k88PPr1

Aqui, tentamos copiar array1 para array2, mas o que estamos fazendo basicamente é apontar array2 para o mesmo local na memória para o qual o array1 aponta. Portanto, tanto o array1 quanto o array2 apontam para o mesmo local na memória e são iguais.

A implicação disso é que, quando fazemos uma mudança no array2, removendo seu último item, o último item de array1 também é removido. Isso ocorre porque a mudança foi feita realmente no array armazenado na memória, enquanto array1 e array2 são apenas indicações para o mesmo local na memória onde o array é armazenado.

Clonando arrays: os hacks

1. Usando Array.prototype.slice()

O método slice() cria uma cópia superficial de uma parte de um array sem modificar o array. Você pode aprender mais sobre a slice() aqui.

O truque é chamar o slice() com 0 como único argumento ou sem nenhum argumento:

// com O como único argumento
array.slice(0);

// sem argumento
array.slice();

Aqui está uma ilustração simples da clonagem de um array com slice():‌  

-XUoysUS92IrVW9lYY6EkJHXv8vKw0yahdaW

Aqui, você pode ver que array2 é um clone de array1, com os mesmos itens e comprimento. No entanto, eles apontam para locais diferentes na memória e, como resultado, não são iguais. Você também percebe que, quando fazemos uma mudança no array2, removendo o último item, o array1 permanece inalterado.

2. Usando Array.prototype.concat()

O método concat() é usado para mesclar dois ou mais arrays, resultando em um novo array, enquanto os arrays originais são deixados inalterados. Você pode saber mais sobre concat() aqui.

O truque é chamar concat() com um array vazio([]) como argumento ou sem nenhum argumento:

// com O como único argumento
array.slice(0);

// sem argumento
array.slice();

‌ Clonando um array com concat() é bastante semelhante ao uso do slice(). Aqui está uma ilustração simples da clonagem de um array com concat():

OXjY30kwODk5622BQraHtZfHxN5d5gewTeZj-1

3. Usando Array.from()

Como vimos anteriormente, Array.from() pode ser usado para criar um array que é uma cópia superficial do array original. Aqui está uma ilustração simples:

kS-1uaQbt9K6zFk4PZWfmnLXHADDnHaVqELf-1

4. Usando a desestruturação de arrays

Com a ES6, temos algumas ferramentas mais poderosas em nossa caixa de ferramentas, como a desestruturação, o operador spread, arrow functions etc. A desestruturação é uma ferramenta muito poderosa para extração de dados de tipos complexos, como arrays e objetos.

O truque é usar uma técnica chamada parâmetros rest, que envolve uma combinação de desestruturação de arrays e o operador spread, como mostrado no seguinte trecho:

let [...arrayClone] = originalArray;

O trecho acima cria uma variável chamada arrayClone, que é um clone de originalArray. Aqui está uma ilustração simples da clonagem de um array usando a desestruturação de arrays:

aohdaDoLpdH1XJ8Thk5U4JE7u0g89qsaTUcI-1

Todas as técnicas de clonagem de array que exploramos até agora produzem uma cópia superficial do array. Isso não será um problema se o array contiver apenas valores primitivos. Entretanto, se o array contiver referências de objetos aninhados, essas referências permanecerão intactas mesmo quando o array for clonado.

Aqui está uma demonstração muito simples disso:

image-6

Observe que a modificação do array aninhado em array1 também modificou o array aninhado em array2 e vice-versa.

A solução para esse problema é criar uma cópia profunda do array. Há algumas maneiras de se fazer isso.

1. A técnica usando JSON

A maneira mais fácil de se criar uma cópia profunda de um array é usando uma combinação de JSON.stringify() e JSON.parse().

JSON.stringify() converte um valor JavaScript em uma string JSON válida, enquanto JSON.parse() converte uma string JSON para um valor ou objeto em JavaScript correspondente.

Aqui temos um exemplo simples:

image-1-1
A técnica de uso de JSON tem algumas falhas, especialmente quando valores que não strings, números e booleanos são envolvidos.

Essas falhas na técnica de uso de JSON podem ser atribuídas principalmente à maneira pela qual o método JSON.stringify() converte valores em strings JSON.

Aqui vemos uma demonstração simples desta falha ao tentar usar JSON.stringify() em um valor contendo uma função aninhada.

image-2-1

2. Auxiliar de cópia profunda

Uma alternativa viável para a técnica de uso de JSON será implementar sua própria função auxiliar de cópia profunda para clonar tipos de referência, sejam eles arrays ou objetos.

Aqui vemos uma função de cópia profunda muito simples e minimalista chamada deepClone:

/**
 * Retorna uma cópia profunda do array ou objeto passado a ele.
 * Útil para copiar objetos e arrays com tipos complexos aninhados.
 *
 * @param {array|objeto} o O valor da cópia profunda, pode ser array ou objeto.
 * @returns {array|objeto} Retorna uma cópia profunda do array ou objeto fornecido.
 */
function deepClone(o) {
  // Constrói a cópia da `saída` do array ou objeto.
  // Use `Array.isArray()` para verificar se `o` é um array.
  // Se `o` é um array, configure a cópia da `saída` para um array vazio (`[]`).
  // Caso contrário, configure a cópia da `saída` para um objeto vazio (`{}`).
  //
  // Se `Array.isArray()` não tem suporte, isto pode ser usado como uma alternativa:
  // Object.prototype.toString.call(o) === '[object Array]'
  // No entanto, é uma alternativa frágil.
  const output = Array.isArray(o) ? [] : {};
  
  // Percorra todas as propriedades de `o`
  for (let i in o) {
    // Obtenha o valor da propriedade atual de `o`
    const value = o[i];
    
    // Se o valor da propriedade atual for um objeto e não for nulo,
    // clone profundamente o valor e atribuá-lo à mesma propriedade na cópia de `saída`.
    // Caso contrário, atribuir o valor bruto a propriedade na cópia de `saída`.
    output[i] = value !== null && typeof value === 'object' ? deepClone(value) : value;
  }
  
  // Retorna a cópia de `saída`.
  return output;
}

Essa, no entanto, não é a melhor das funções de cópia profunda, como você verá em breve com algumas bibliotecas JavaScript. No entanto, ela realiza cópias profundas de modo bastante razoável.

image-3-1

3. Usando bibliotecas JavaScript

A função do auxiliar de cópia profunda que acabamos de definir não é suficientemente robusta para clonar todos os tipos de dados em JavaScript que podem estar aninhados dentro de objetos ou arrays complexos.

As bibliotecas do JavaScript, como Lodash e jQuery, oferecem funções utilitárias de cópia profunda mais robustas e com suporte para diferentes tipos de dados em JavaScript.

Aqui está um exemplo que usa _.cloneDeep() da biblioteca de Lodash:

image-4-1

Aqui está o mesmo exemplo, mas usando $.extend() da biblioteca jQuery:

image-5-1

Conclusão

Neste artigo, pudemos explorar várias técnicas para criar arrays dinamicamente e clonar arrays existentes, incluindo a conversão de objetos semelhantes a arrays e iteráveis para arrays.

Também vimos como algumas das novas características e melhorias introduzidas na ES6 podem nos permitir realizar efetivamente certas manipulações nos arrays.

Utilizamos características como desestruturação e o operador spread para clonagem e espalhamento (do inglês, spreading) dos arrays. Você pode saber mais sobre desestruturação neste artigo (texto em inglês).

Fique à vontade para seguir o autor no Medium (Glad Chinda) para artigos mais esclarecedores que você pode achar útil. Você também pode seguir o autor no Twitter (@gladchinda).

Boa programação para você!