Qué es el protocolo de iteración
Cuando en JavaScript escribes for (const x of lista) o [...iterable], el motor no asume que estás trabajando con un array. Lo que hace es invocar el protocolo de iteración: un contrato formado por dos piezas.
- Un objeto es iterable si tiene una propiedad con la clave
Symbol.iterator, cuyo valor es una función que devuelve un iterador. - Un objeto es un iterador si tiene un método
next()que devuelve un objeto{ value, done }. Cuandodonevaletrue, la iteración ha terminado.
const numbers = [10, 20, 30];
const iterator = numbers[Symbol.iterator]();
console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Los arrays, strings, Map, Set y arguments ya implementan este protocolo. Cualquier estructura que quiera participar en for...of, spread o desestructuración debe exponer Symbol.iterator.
Crear un iterable personalizado
Veamos cómo convertir una clase propia en algo iterable. El ejemplo clásico es un rango numérico:
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const { end, step } = this;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
const range = new Range(1, 10, 2);
for (const n of range) {
console.log(n); // 1, 3, 5, 7, 9
}
console.log([...range]); // [1, 3, 5, 7, 9]
console.log(Array.from(range, n => n ** 2)); // [1, 9, 25, 49, 81]
Lo importante es que el iterador es independiente del iterable: cada llamada a Symbol.iterator produce uno nuevo. Por eso podemos recorrer el rango dos veces sin agotarlo.
Iterador que además es iterable
Un patrón cómodo es que el propio iterador sea iterable (se devuelva a sí mismo en Symbol.iterator). Así puedes reanudar una iteración a medias desde un for...of:
function counter() {
let i = 0;
return {
next() { return { value: i++, done: false }; },
[Symbol.iterator]() { return this; }
};
}
const c = counter();
c.next(); // { value: 0 }
c.next(); // { value: 1 }
for (const n of c) {
if (n > 4) break;
console.log(n); // 2, 3, 4
}
Funciones generadoras
Escribir iteradores a mano es verboso. Las funciones generadoras (function*) hacen todo el trabajo: basta con usar yield y el motor construye por ti un iterador compatible con el protocolo.
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
for (const n of range(1, 10, 2)) {
console.log(n); // 1, 3, 5, 7, 9
}
Lo que sucede por debajo es lo mismo que el ejemplo manual: llamar a range(1, 10, 2) no ejecuta el cuerpo, sino que devuelve un iterador. Cada next() reanuda la función hasta el siguiente yield, y devuelve { value, done: false }. Cuando la función retorna, el resultado pasa a { value: undefined, done: true }.
Estado preservado entre yields
El generador guarda todo su estado local entre llamadas, lo que permite lógicas que serían incómodas con iteradores manuales:
function* pairs(array) {
for (let i = 0; i < array.length; i += 2) {
yield [array[i], array[i + 1]];
}
}
console.log([...pairs(["a", 1, "b", 2, "c", 3])]);
// [ ["a", 1], ["b", 2], ["c", 3] ]
Secuencias infinitas y pereza
Al ejecutarse paso a paso, un generador puede modelar una secuencia infinita sin colapsar la memoria. El consumidor decide cuántos valores toma:
function* naturals() {
let i = 1;
while (true) {
yield i++;
}
}
function take(iterable, count) {
const result = [];
for (const value of iterable) {
if (result.length >= count) break;
result.push(value);
}
return result;
}
console.log(take(naturals(), 5)); // [1, 2, 3, 4, 5]
Esta naturaleza perezosa es especialmente útil para representar streams de datos, cursores paginados o cálculos caros que no siempre se consumen completos.
Delegación con yield*
yield* delega la producción de valores a otro iterable: todos sus valores pasan a formar parte de la secuencia del generador actual. Es la manera idiomática de componer generadores.
function* letters() {
yield "a";
yield "b";
yield "c";
}
function* digits() {
yield 1;
yield 2;
yield 3;
}
function* mix() {
yield* letters();
yield "-";
yield* digits();
}
console.log([...mix()]); // ["a", "b", "c", "-", 1, 2, 3]
Como los arrays ya son iterables, también se pueden delegar directamente:
function* flatten(array) {
for (const item of array) {
if (Array.isArray(item)) {
yield* flatten(item);
} else {
yield item;
}
}
}
console.log([...flatten([1, [2, [3, [4, 5]]], 6])]); // [1, 2, 3, 4, 5, 6]
El aplanado recursivo queda trivial y, al ser un generador, es perezoso: consume el árbol solo a medida que lo recorres.
Pipelines perezosos
Combinando varios generadores se obtienen pipelines de transformación equivalentes a map y filter, pero sin crear arrays intermedios:
function* map(iterable, fn) {
for (const value of iterable) yield fn(value);
}
function* filter(iterable, predicate) {
for (const value of iterable) {
if (predicate(value)) yield value;
}
}
function* take(iterable, count) {
let i = 0;
for (const value of iterable) {
if (i++ >= count) return;
yield value;
}
}
// Primeros 5 cuadrados pares de los naturales
const result = take(
filter(
map(naturals(), n => n * n),
n => n % 2 === 0
),
5
);
console.log([...result]); // [4, 16, 36, 64, 100]
Solo se calcula lo estrictamente necesario para producir los 5 valores pedidos, aunque la fuente (naturals) sea infinita.
Comunicación bidireccional: next(value) y return
El método next acepta un argumento que se convierte en el valor de la expresión yield dentro del generador. Esto permite que el consumidor inyecte datos en la función:
function* dialog() {
const name = yield "¿Cómo te llamas?";
const age = yield `Hola, ${name}. ¿Cuántos años tienes?`;
return `${name} tiene ${age} años.`;
}
const it = dialog();
console.log(it.next().value); // "¿Cómo te llamas?"
console.log(it.next("Alan").value); // "Hola, Alan. ¿Cuántos años tienes?"
console.log(it.next(34).value); // "Alan tiene 34 años."
Además de next, un iterador puede exponer return(value) para terminar de forma anticipada y throw(error) para inyectar una excepción dentro del generador. for...of invoca return cuando sale del bucle con break, lo que permite a los generadores ejecutar código de limpieza con try/finally.
Buenas prácticas
- Usa
for...ofo spread siempre que puedas; evita llamar anext()a mano salvo que necesites una iteración manual muy concreta. - Prefiere generadores a iteradores escritos a mano: son más cortos, más claros y gestionan solos los estados terminales.
- Aprovecha
yield*para componer generadores en lugar de copiar lógica; permite encadenar transformaciones sin coste de memoria. - Para secuencias potencialmente infinitas, define un combinador como
takeque limite la cantidad tomada; nunca hagas[...infinite]. - Si el generador mantiene recursos externos (un cursor de base de datos, un archivo), libéralos en un bloque
finallypara que se ejecuten también enbreakothrow.
Resumen
Iteradores y generadores son las piezas que hacen posibles construcciones como for...of, spread y la desestructuración. Implementar Symbol.iterator convierte cualquier objeto en iterable, y function* + yield reduce ese trabajo a unas pocas líneas, con soporte natural para secuencias perezosas, infinitas y composables. Una vez interiorizado el protocolo, se abre la puerta a diseñar APIs que procesan datos streaming sin cargar todo en memoria.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, JavaScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de JavaScript
Explora más contenido relacionado con JavaScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Entender el protocolo iterable/iterator y cómo lo usa
for...of. - Implementar un iterable personalizado con
Symbol.iterator. - Crear funciones generadoras con
function*y controlar el flujo conyield. - Producir secuencias perezosas e incluso infinitas sin colapsar la memoria.
- Combinar generadores con
yield*para delegar en otros iterables. - Construir pipelines de transformación basados en iteradores.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje