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.
