Iteradores y generadores ES6

Los iteradores y generadores suelen ser un pensamiento secundario al escribir código, pero si puede tomarse unos minutos para pensar en cómo usarlos para simplificar su co...

Los iteradores y generadores suelen ser un pensamiento secundario al escribir código, pero si puede tomarse unos minutos para pensar en cómo usarlos para simplificar su código, le ahorrarán mucha depuración y complejidad. Con los nuevos iteradores y generadores de ES6, JavaScript obtiene una funcionalidad similar a Iterable de Java, lo que nos permite personalizar nuestra iteración en los objetos.

Por ejemplo, si tenía un objeto Graph, puede usar fácilmente un generador para recorrer los nodos o los bordes. Esto hace que el código sea mucho más limpio al colocar la lógica transversal dentro del objeto Graph donde pertenece. Esta separación de la lógica es una buena práctica, y los iteradores/generadores facilitan el seguimiento de estas mejores prácticas.

Iteradores y generadores de ES6

Iteradores

Usando iteradores, puede crear una forma de iterar usando la construcción for...of para su objeto personalizado. En lugar de usar for...in, que simplemente itera a través de todas las propiedades del objeto, usar for...of nos permite crear un iterador mucho más personalizado y estructurado en el que elegimos qué valores devolver para cada iteración.

Bajo el capó, for...of en realidad está usando Symbol.iterator. Recall simbolos son una característica nueva en JavaScript ES6. El Symbol.iterator es un símbolo de propósito especial hecho especialmente para acceder al iterador interno de un objeto. Entonces, podría usarlo para recuperar una función que itera sobre un objeto de matriz, así:

var nums = [6, 7, 8];
var iterator = nums[Symbol.iterator]();
iterator.next();                // Returns { value: 6, done: false }
iterator.next();                // Returns { value: 7, done: false }
iterator.next();                // Returns { value: 8, done: false }
iterator.next();                // Returns { value: undefined, done: true }

Cuando usa la construcción for...of, esto es en realidad lo que se usa debajo. Observe cómo se devuelve cada valor subsiguiente, junto con un indicador que le indica si está en la La gran mayoría de las veces no necesitará usar next() manualmente de esta manera, pero la opción está ahí en caso de que tenga un caso de uso que requiera un bucle más complejo.

Puedes usar Symbol.iterator para definir una iteración especializada para un objeto. Así que digamos que tienes tu propio objeto que es un envoltorio para oraciones, Sentence.

function Sentence(str) {
    this._str = str;
}

Para definir cómo iteramos sobre las partes internas del objeto Sentence, proporcionamos una función iteradora prototipo:

Sentence.prototype[Symbol.iterator] = function() {
    var re = /\S+/g;
    var str = this._str;

    return {
        next: function() {
            var match = re.exec(str);
            if (match) {
                return {value: match[0], done: false};
            }
            return {value: undefined, done: true};
        }
    }
};

Ahora, usando el iterador que acabamos de crear arriba (que contiene una cadena de expresiones regulares que coincide solo con palabras), podemos iterar fácilmente sobre las palabras de cualquier oración que proporcionemos:

var s = new Sentence('Good day, kind sir.');

for (var w of s) {
    console.log(w);
}

// Prints:
// Good
// day,
// kind
// sir.
Generadores

Los generadores ES6 se basan en lo que proporcionan los iteradores mediante el uso de una sintaxis especial para crear más fácilmente la función de iteración. Los generadores se definen usando la palabra clave function*. Dentro de una función*, puedes devolver valores repetidamente usando yield. La palabra clave yield se usa en funciones generadoras para pausar la ejecución y devolver un valor. Se puede considerar como una versión basada en un generador de la palabra clave return. En la siguiente iteración, la ejecución se reanudará en el último punto en el que se utilizó yield.

function* myGenerator() {
    yield 'foo';
    yield 'bar';
    yield 'baz';
}

myGenerator.next();     // Returns {value: 'foo', done: false}
myGenerator.next();     // Returns {value: 'bar', done: false}
myGenerator.next();     // Returns {value: 'baz', done: false}
myGenerator.next();     // Returns {value: undefined, done: true}

O bien, podría usar la construcción for...of:

for (var n of myGenerator()) {
    console.log(n);
}

// Prints
// foo
// bar
// baz

En este caso, podemos ver que el Generador luego se encarga de devolver el objeto {value: val, done: bool}, que se maneja bajo el capó en for...of.

Entonces, ¿cómo podemos usar los generadores a nuestro favor? Volviendo a nuestro ejemplo anterior, podemos simplificar el iterador Sentence al siguiente código:

Sentence.prototype[Symbol.iterator] = function*() {
    var re = /\S+/g;
    var str = this._str;
    var match;
    while (match = re.exec(str)) {
        yield match[0];
    }
};

Observe cómo la función del iterador (ahora un Generador) es mucho más pequeña que la versión anterior. Ya no necesitamos devolver un objeto con la función siguiente, y ya no necesitamos lidiar con devolver el objeto {value: val, done: bool}. Si bien estos ahorros pueden parecer mínimos en este ejemplo, su utilidad se realizará fácilmente a medida que sus generadores crezcan en complejidad.

Ventajas

Como señala Jake Archibald, algunas ventajas de estos generadores son:

  • Pereza: los valores no se calculan con anticipación, por lo que si no itera hasta el final, no habrá perdido el tiempo calculando los valores no utilizados.

  • Infinito: dado que los valores no se calculan con anticipación, puede obtener un conjunto infinito de valores. Solo asegúrate de salir del bucle en algún momento.

  • Iteración de cadenas: gracias a Symbol.iterator, String ahora tiene su propio iterador para hacer que el bucle de caracteres sea mucho más fácil. Iterar sobre los símbolos de caracteres de una cadena puede ser un verdadero dolor de cabeza. Esto es especialmente útil ahora que JavaScript ES5 es compatible con Unicode.

    for (var symbol of string) {
    console.log(symbol);
    }

Conclusión

Si bien los iteradores y los generadores no son grandes funciones adicionales, ayudan bastante a limpiar el código y mantenerlo organizado. Mantener la lógica de iteración con el objeto al que pertenece es una buena práctica, que parece ser una gran parte del enfoque de las características de ES6. El estándar parece estar moviéndose hacia la capacidad de estructura y la facilidad de diseño de Java, mientras mantiene la velocidad de desarrollo de los lenguajes dinámicos.

¿Qué opinas de las nuevas características de ES6? ¿Qué tipo de casos de uso interesantes tiene para iteradores y generadores? ¡Cuéntanos en los comentarios!

Gracias a Jake Archibald por el gran artículo que detalla gran parte de cómo funcionan los iteradores y generadores en ES6.