Cómo usar generadores e iteradores en JavaScript
La iteración de recopilaciones de datos mediante bucles tradicionales puede volverse rápidamente engorrosa y lenta, especialmente cuando se trata de grandes cantidades de datos.
Los generadores e iteradores de JavaScript brindan una solución para iterar de manera eficiente sobre grandes colecciones de datos. Utilizándolos, puede controlar el flujo de iteración, generar valores de uno en uno y pausar y reanudar el proceso de iteración.
Aquí cubrirá los aspectos básicos e internos de un iterador de JavaScript y cómo puede generar un iterador manualmente y usando un generador.
Iteradores de JavaScript
Un iterador es un objeto JavaScript que implementa el protocolo iterador. Estos objetos lo hacen al tener un método next . Este método devuelve un objeto que implementa la interfaz IteratorResult .
La interfaz IteratorResult consta de dos propiedades: done y value . La propiedad done es un valor booleano que devuelve falso si el iterador puede producir el siguiente valor en su secuencia o verdadero si el iterador ha completado su secuencia.
La propiedad de valor es un valor de JavaScript devuelto por el iterador durante su secuencia. Cuando un iterador completa su secuencia (cuando se hace === true ), esta propiedad devuelve undefined .
Como su nombre lo indica, los iteradores le permiten «iterar» sobre objetos de JavaScript, como matrices o mapas. Este comportamiento es posible debido al protocolo iterable.
En JavaScript, el protocolo iterable es una forma estándar de definir objetos sobre los que puede iterar, como en un bucle for…of .
Por ejemplo:
const fruits = ["Banana", "Mango", "Apple", "Grapes"];
for (const iterator of fruits) {
console.log(iterator);
}
/*
Banana
Mango
Apple
Grapes
*/
Este ejemplo itera sobre la matriz de frutas usando un bucle for…of . En cada iteración, registra el valor actual en la consola. Esto es posible porque las matrices son iterables.
Algunos tipos de JavaScript, como Arrays, Strings, Sets y Maps, son iterables integrados porque ellos (o uno de los objetos en su cadena de prototipo) implementan un método @@ iterator .
Otros tipos, como los objetos, no se pueden iterar de forma predeterminada.
Por ejemplo:
const iterObject = {
cars: ["Tesla", "BMW", "Toyota"],
animals: ["Cat", "Dog", "Hamster"],
food: ["Burgers", "Pizza", "Pasta"],
};
for (const iterator of iterObject) {
console.log(iterator);
}
// TypeError: iterObject is not iterable
Este ejemplo demuestra lo que sucede cuando intenta iterar sobre un objeto que no es iterable.
Hacer un objeto iterable
Para hacer que un objeto sea iterable, debe implementar un método Symbol.iterator en el objeto. Para volverse iterable, este método debe devolver un objeto que implemente la interfaz IteratorResult .
Los bloques de código a continuación proporcionan un ejemplo de cómo hacer que un objeto sea iterable usando iterObject .
Primero, agregue el método Symbol.iterator a iterObject usando una declaración de función.
Al igual que:
iterObject[Symbol.iterator] = function () {
// Subsequent code blocks go here...
}
A continuación, deberá acceder a todas las claves en el objeto que desea hacer iterable. Puede acceder a las claves utilizando el método Object.keys , que devuelve una matriz de las propiedades enumerables de un objeto. Para devolver una matriz de claves de iterObject , pase la palabra clave this como argumento a Object.keys .
Por ejemplo:
let objProperties = Object.keys(this)
El acceso a esta matriz le permitirá definir el comportamiento de iteración del objeto.
A continuación, debe realizar un seguimiento de las iteraciones del objeto. Puede lograr esto usando variables de contador.
Por ejemplo:
let propertyIndex = 0;
let childIndex = 0;
Utilizará la primera variable de contador para realizar un seguimiento de las propiedades del objeto y la segunda para realizar un seguimiento de los elementos secundarios de la propiedad.
A continuación, deberá implementar y devolver el siguiente método.
Al igual que:
return {
next() {
// Subsequent code blocks go here...
}
}
Dentro del siguiente método, deberá manejar un caso límite que ocurre cuando se ha iterado todo el objeto. Para manejar el caso límite, debe devolver un objeto con el valor establecido en undefined y done establecido en true .
Aquí se explica cómo manejar el caso extremo:
if (propertyIndex > objProperties.length - 1) {
return {
value: undefined,
done: true,
};
}
A continuación, deberá acceder a las propiedades del objeto y sus elementos secundarios utilizando las variables de contador que declaró anteriormente.
Al igual que:
// Accessing parent and child properties
const properties = this[objProperties[propertyIndex]];
const property = properties[childIndex];
A continuación, debe implementar alguna lógica para incrementar las variables del contador. La lógica debería restablecer childIndex cuando no existan más elementos en la matriz de una propiedad y pasar a la siguiente propiedad en el objeto. Además, debería incrementar childIndex , si todavía hay elementos en la matriz de la propiedad actual.
Por ejemplo:
// Index incrementing logic
if (childIndex >= properties.length - 1) {
// if there are no more elements in the child array
// reset child index
childIndex = 0;
// Move to the next property
propertyIndex++;
} else {
// Move to the next element in the child array
childIndex++
}
Finalmente, devuelve un objeto con la propiedad done establecida en false y la propiedad value establecida en el elemento secundario actual en la iteración.
Por ejemplo:
return {
done: false,
value: property,
};
Su función Symbol.iterator completada debe ser similar al bloque de código a continuación:
iterObject[Symbol.iterator] = function () {
const objProperties = Object.keys(this);
let propertyIndex = 0;
let childIndex = 0;
return {
next: () => {
//Handling edge case
if (propertyIndex > objProperties.length - 1) {
return {
value: undefined,
done: true,
};
}
// Accessing parent and child properties
const properties = this[objProperties[propertyIndex]];
const property = properties[childIndex];
// Index incrementing logic
if (childIndex >= properties.length - 1) {
// if there are no more elements in the child array
// reset child index
childIndex = 0;
// Move to the next property
propertyIndex++;
} else {
// Move to the next element in the child array
childIndex++
}
return {
done: false,
value: property,
};
},
};
};
Ejecutar un bucle for…of en iterObject después de esta implementación no generará un error ya que implementa un método Symbol.iterator .
No se recomienda implementar iteradores manualmente, como hicimos anteriormente, ya que es muy propenso a errores y la lógica puede ser difícil de administrar.
Generadores de JavaScript
Un generador de JavaScript es una función que puede pausar y reanudar su ejecución en cualquier momento. Este comportamiento le permite producir una secuencia de valores a lo largo del tiempo.
Una función generadora, que es una función que devuelve un Generador, proporciona una alternativa a la creación de iteradores.
Puede crear una función generadora de la misma manera que crearía una declaración de función en JavaScript. La única diferencia es que debe agregar un asterisco ( * ) a la palabra clave de función.
Por ejemplo:
function* example () {
return "Generator"
}
Cuando llama a una función normal en JavaScript, devuelve el valor especificado por su palabra clave de retorno o no definido de otra manera. Pero una función generadora no devuelve ningún valor inmediatamente. Devuelve un objeto Generador, que puede asignar a una variable.
Para acceder al valor actual del iterador, llame al siguiente método en el objeto Generador.
Por ejemplo:
const gen = example();
console.log(gen.next()); // { value: 'Generator', done: true }
En el ejemplo anterior, la propiedad de valor provino de una palabra clave de retorno , terminando efectivamente el generador. Este comportamiento generalmente no es deseable con las funciones del generador, ya que lo que las distingue de las funciones normales es la capacidad de pausar y reiniciar la ejecución.
La palabra clave de rendimiento
La palabra clave yield proporciona una forma de iterar a través de valores en generadores al pausar la ejecución de una función de generador y devolver el valor que le sigue.
Por ejemplo:
function* example() {
yield "Model S"
yield "Model X"
yield "Cyber Truck"
return "Tesla"
}
const gen = example();
console.log(gen.next()); // { value: 'Model S', done: false }
En el ejemplo anterior, cuando se llama al siguiente método en el generador de ejemplo , se detendrá cada vez que encuentre la palabra clave yield . La propiedad done también se establecerá en false hasta que encuentre una palabra clave de retorno .
Llamando al siguiente método varias veces en el generador de ejemplo para demostrar esto, obtendrá lo siguiente como salida.
console.log(gen.next()); // { value: 'Model X', done: false }
console.log(gen.next()); // { value: 'Cyber Truck', done: false }
console.log(gen.next()); // { value: 'Tesla', done: true }
console.log(gen.next()); // { value: undefined, done: true }
También puede iterar sobre un objeto Generador usando el bucle for…of .
Por ejemplo:
for (const iterator of gen) {
console.log(iterator);
}
/*
Model S
Model X
Cyber Truck
*/
Uso de iteradores y generadores
Aunque los iteradores y los generadores pueden parecer conceptos abstractos, no lo son. Pueden ser útiles cuando se trabaja con flujos de datos y colecciones de datos infinitos. También puede usarlos para crear identificadores únicos. Las bibliotecas de administración de estado, como MobX-State-Tree (MST), también las usan bajo el capó.
Deja una respuesta