Os laços for...of e for...in são duas construções poderosas em JavaScript para percorrer coleções de dados. Embora pareçam semelhantes, eles têm propósitos e comportamentos distintos que podem gerar confusão. Nesta aula, vamos mergulhar em cada um deles, entender quando usar cada um e, principalmente, quais armadilhas evitar.

Iterando arrays e objetos

O laço for...of foi introduzido no ES6 e é projetado para iterar sobre objetos iteráveis, como arrays, strings, Map, Set, etc. Ele percorre os valores dos elementos, ignorando propriedades não numéricas ou herdadas. Por outro lado, for...in itera sobre as propriedades enumeráveis de um objeto, incluindo as herdadas do protótipo. É importante notar que for...in não é recomendado para arrays, pois pode trazer surpresas.

Vamos ver exemplos práticos:

// Exemplo com for...of em array
const frutas = ['maçã', 'banana', 'laranja'];
for (const fruta of frutas) {
  console.log(fruta); // 'maçã', 'banana', 'laranja'
}

// Exemplo com for...in em array
for (const indice in frutas) {
  console.log(indice); // '0', '1', '2' (strings!)
  console.log(frutas[indice]); // 'maçã', 'banana', 'laranja'
}

// Exemplo com for...in em objeto
const pessoa = { nome: 'João', idade: 30 };
for (const chave in pessoa) {
  console.log(chave + ': ' + pessoa[chave]);
}
// nome: João
// idade: 30

// for...of em objeto (não funciona diretamente)
// for (const valor of pessoa) { // TypeError: pessoa is not iterable
//   console.log(valor);
// }

No código acima, vemos que for...of itera sobre os valores do array, enquanto for...in itera sobre as chaves (índices) como strings. Para objetos, for...in funciona bem para acessar propriedades próprias e enumeráveis. Já for...of requer que o objeto seja iterável, o que objetos comuns não são, a menos que implementem o protocolo de iteração.

Diferença crucial entre eles

A diferença fundamental é que for...in itera sobre as chaves (propriedades) de um objeto, enquanto for...of itera sobre os valores de um iterável. Isso tem implicações importantes:

  • Tipo de dado retornado: for...in retorna strings (nomes das propriedades); for...of retorna os próprios valores.
  • Escopo de uso: for...of funciona apenas com iteráveis (arrays, strings, Map, Set, etc.), enquanto for...in funciona com qualquer objeto.
  • Herança: for...in percorre também propriedades herdadas do protótipo, a menos que sejam não enumeráveis. for...of não se preocupa com herança.
  • Performance: for...of tende a ser mais rápido em arrays porque não precisa lidar com a cadeia de protótipos.

Veja um exemplo que ilustra a herança:

function Animal() {}
Animal.prototype.som = 'grr';

const cachorro = new Animal();
cachorro.nome = 'Rex';

// for...in inclui 'som' (herdado) e 'nome'
for (const prop in cachorro) {
  if (cachorro.hasOwnProperty(prop)) {
    console.log('Própria: ' + prop); // apenas 'nome'
  } else {
    console.log('Herdada: ' + prop); // 'som'
  }
}

// for...of não se aplica a objetos comuns

Para evitar propriedades herdadas, use o método hasOwnProperty dentro do loop for...in.

Pegadinhas

Algumas armadilhas comuns ao usar esses laços:

  • Usar for...in em arrays: Como vimos, for...in itera sobre índices como strings e também pode incluir propriedades adicionadas ao array (ex.: array.novaProp = 'valor'). Além disso, a ordem de iteração não é garantida, embora na prática seja numérica para índices.
  • Esquecer que for...in inclui propriedades herdadas: Se você estender o protótipo de Array ou Object, isso afetará o loop. Sempre use hasOwnProperty ou prefira for...of para arrays.
  • Tentar usar for...of em objetos comuns: Isso gera um erro. Para iterar sobre objetos, use Object.keys(), Object.values() ou Object.entries() combinados com for...of.
  • Confundir a variável de iteração: Em for...in, a variável recebe o nome da chave (string). Em for...of, recebe o valor. Misturar isso pode causar bugs.

Exemplo de pegadinha com for...in em array:

const arr = [10, 20, 30];
arr.novaProp = 'Olá';

for (const i in arr) {
  console.log(i + ': ' + arr[i]);
}
// 0: 10
// 1: 20
// 2: 30
// novaProp: Olá

Já com for...of, a propriedade extra é ignorada:

for (const valor of arr) {
  console.log(valor); // 10, 20, 30 (apenas)
}

Boas práticas

Para evitar problemas, siga estas recomendações:

  • Use for...of para arrays, strings, Map, Set e outros iteráveis.
  • Use for...in apenas para objetos simples (não arrays) quando precisar das chaves.
  • Sempre filtre propriedades herdadas com hasOwnProperty dentro de for...in.
  • Considere usar métodos como forEach, map, filter para arrays, que são mais expressivos e menos propensos a erros.

Referências

Exercícios

  1. Dado o array const nums = [1, 2, 3];, use for...of para imprimir cada número ao quadrado.

    ✓ Resposta:
    const nums = [1, 2, 3];
    for (const num of nums) {
      console.log(num * num);
    }
    
  2. Use for...in para listar as propriedades do objeto const carro = { marca: 'Fiat', modelo: 'Uno' };, incluindo as herdadas. Depois, modifique para listar apenas as propriedades próprias.

    ✓ Resposta:
    const carro = { marca: 'Fiat', modelo: 'Uno' };
    // Todas as propriedades (incluindo herdadas)
    for (const prop in carro) {
      console.log(prop + ': ' + carro[prop]);
    }
    // Apenas próprias
    for (const prop in carro) {
      if (carro.hasOwnProperty(prop)) {
        console.log(prop + ': ' + carro[prop]);
      }
    }
    
  3. Explique por que o código a seguir pode gerar resultados inesperados: const arr = [1, 2, 3]; Array.prototype.extra = 'X'; for (const i in arr) { console.log(i); }

    ✓ Resposta:

    O for...in itera sobre todas as propriedades enumeráveis, incluindo a propriedade extra adicionada ao protótipo de Array. Portanto, além dos índices '0', '1', '2', também será impresso 'extra'. Para evitar isso, use for...of ou verifique hasOwnProperty.

  4. Dado o objeto const pessoa = { nome: 'Ana', idade: 25 };, use for...of com Object.entries() para imprimir chave e valor.

    ✓ Resposta:
    const pessoa = { nome: 'Ana', idade: 25 };
    for (const [chave, valor] of Object.entries(pessoa)) {
      console.log(chave + ': ' + valor);
    }
    
  5. Qual é a saída do código abaixo? Justifique.

    const str = 'abc';
    for (const letra of str) {
      console.log(letra.toUpperCase());
    }
    

    ✓ Resposta:

    A saída será 'A', 'B', 'C' (cada um em uma linha). O laço for...of itera sobre os valores da string, que são cada caractere. O método toUpperCase() converte para maiúscula.