Iteradores y el protocolo iterable en JavaScript: for...of desde dentro

El protocolo iterable de JavaScript define cómo los objetos pueden ser recorridos por construcciones del lenguaje como for...of, el spread operator, el destructuring y Array.from(). Entender este protocolo permite crear tus propias colecciones que se integran de forma natural con todas estas herramientas.

El protocolo iterable

Un objeto es iterable si tiene un método [Symbol.iterator]() que devuelve un iterador. Un iterador es un objeto con un método next() que devuelve { value, done }:

// Un array es iterable nativo
const arr = [1, 2, 3];

// Obtener el iterador manualmente
const iterador = arr[Symbol.iterator]();

console.log(iterador.next());  // { value: 1, done: false }
console.log(iterador.next());  // { value: 2, done: false }
console.log(iterador.next());  // { value: 3, done: false }
console.log(iterador.next());  // { value: undefined, done: true }

// for...of hace exactamente esto por ti:
for (const valor of arr) {
  console.log(valor);  // 1, 2, 3
}

// Estas construcciones también usan el protocolo iterable:
const [a, b] = arr;           // destructuring
const copia = [...arr];        // spread
const arr2 = Array.from(arr);  // Array.from

Hacer iterable un objeto propio

Para que tu objeto funcione con for...of, solo necesita implementar [Symbol.iterator]():

class Rango {
  constructor(inicio, fin, paso = 1) {
    this.inicio = inicio;
    this.fin = fin;
    this.paso = paso;
  }

  [Symbol.iterator]() {
    let actual = this.inicio;
    const fin = this.fin;
    const paso = this.paso;

    return {
      next() {
        if (actual <= fin) {
          const valor = actual;
          actual += paso;
          return { value: valor, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const rango = new Rango(0, 10, 2);
console.log([...rango]);        // [0, 2, 4, 6, 8, 10]

for (const n of new Rango(1, 5)) {
  process.stdout.write(n + ' ');  // 1 2 3 4 5
}

// También funciona con destructuring y Array.from:
const [primero, segundo] = new Rango(10, 50, 10);
console.log(primero, segundo);  // 10 20

Iterador que también es iterable

Por convención, los iteradores deberían ser también iterables (devolviendo this desde su [Symbol.iterator]()). Esto permite usarlos en cualquier contexto que espere un iterable:

function crearIterador(datos) {
  let indice = 0;
  return {
    next() {
      if (indice < datos.length) {
        return { value: datos[indice++], done: false };
      }
      return { value: undefined, done: true };
    },
    // El propio iterador es iterable
    [Symbol.iterator]() {
      return this;
    }
  };
}

const iter = crearIterador(['a', 'b', 'c']);

// Funciona en for...of porque también es iterable:
for (const v of iter) {
  console.log(v);  // 'a', 'b', 'c'
}

El error clásico: confundir array-like con iterable

Un objeto array-like tiene índices y length pero no es iterable. Un iterable tiene [Symbol.iterator] pero puede no tener índices ni length:

// Array-like: tiene índices y length, pero NO es iterable
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };

for (const v of arrayLike) { }  // TypeError: arrayLike is not iterable
console.log([...arrayLike]);    // TypeError

// Para convertir array-like a array real:
const arr = Array.from(arrayLike);  // ['a', 'b', 'c']
const arr2 = [...arrayLike];        // TypeError (no iterable)

// NodeList: es iterable (tiene Symbol.iterator)
const nodos = document.querySelectorAll('p');
for (const p of nodos) {           // OK
  console.log(p.textContent);
}

// arguments: es array-like e iterable
function ejemplo() {
  for (const arg of arguments) {  // OK: arguments es iterable
    console.log(arg);
  }
}

Iterables integrados en JavaScript

Muchos tipos nativos ya implementan el protocolo iterable:

// String: itera carácter a carácter (con soporte Unicode correcto)
for (const char of 'hola') console.log(char);  // 'h', 'o', 'l', 'a'
console.log([...'emoji: ?']);  // ['e', 'm', 'o', 'j', 'i', ':', ' ', '?']

// Map y Set
const mapa = new Map([['a', 1], ['b', 2]]);
for (const [clave, valor] of mapa) console.log(clave, valor);

// Destructuring de Map:
const [[primeraC, primerV]] = mapa;
console.log(primeraC, primerV);  // 'a', 1

// Spread de Set:
const set = new Set([1, 2, 3]);
console.log([...set]);  // [1, 2, 3]

// Generators implementan el protocolo automáticamente:
function* gen() { yield 1; yield 2; yield 3; }
console.log([...gen()]);  // [1, 2, 3]

El protocolo iterable es uno de los puntos de extensión más potentes de JavaScript: cualquier objeto que lo implemente se integra automáticamente con for...of, spread, destructuring, Array.from(), Promise.all(), new Map() y new Set(), sin necesidad de ninguna conversión intermedia.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP