Pregunta 1Si llamas dos veces a countDownload() y luego muestras total en el nivel más externo, ¿qué pasa?
Closures — funciones que recuerdan variables externas
Los closures recuerdan variables de un ámbito externo. Incluye contadores que persisten entre llamadas, fábricas de funciones y funciones creadas en bucles.
A veces quieres que una función guarde un valor que aumenta cada vez que la llamas, como las visitas de una página. Pero si cuentas con una variable común, o el valor no se conserva, o cualquier línea puede sobrescribirlo.
Este artículo trata los closures, funciones que recuerdan variables de un ámbito externo, y las fábricas de funciones, funciones que devuelven otra función con un valor de configuración ya fijado.
Conservar un valor entre llamadas — lo básico de los closures
Supón que quieres contar las descargas de un documento. Si declaras un let dentro de la función que cuenta, vuelve a 0 en cada llamada. Si lo declaras en el nivel más externo, el valor se conserva, pero queda en un lugar donde cualquier línea puede sobrescribirlo.
Con un closure (una función que recuerda variables de un ámbito externo), puedes conservar un valor entre llamadas y, al mismo tiempo, impedir que las líneas de afuera lo sobrescriban. Si declaras una variable dentro de una función y devuelves con return otra función que la lee y la modifica, la función devuelta sale llevándose esa variable.
function makeDownloadCounter() {
let total = 0; // Variable que solo existe dentro de makeDownloadCounter
const countUp = () => {
total = total + 1; // Modifica el total de afuera
return total;
};
return countUp; // Devuelve la función misma
}
const countDownload = makeDownloadCounter(); // Crea un contador
console.log(countDownload()); // 1
console.log(countDownload()); // 2
console.log(countDownload()); // 3 (sigue desde el valor anterior)
// console.log(total); // ReferenceError: total is not defined
const countDownload = makeDownloadCounter()— guarda la función devueltaconsole.log(total)— el nombre no es visible, así que daReferenceError
let total = 0— llamar acountDownload()no lo devuelve a 0return countUp— devuelve la función misma, no un valor
total = total + 1— modifica el total de afuera- Puede seguir leyendo y modificando este total después de que la función externa terminó
countUp se usa desde el nivel más externo y sigue recordando el total de afuera. Aunque la función termine, la variable recordada se conserva.La función que adentro se llama countUp se usa afuera con el nombre countDownload. Lo que entrega return es la función misma, así que a la variable que la recibe le puedes poner el nombre que mejor encaje donde la uses. La tabla de abajo compara, según dónde pongas el valor que cuentas, si se conserva entre llamadas y si se puede sobrescribir.
| Dónde vive el conteo | Valor entre llamadas | ¿Lo pueden sobrescribir líneas de afuera? |
|---|---|---|
| Declarar let dentro de la función que cuenta | Desaparece al terminar la llamada; la siguiente empieza en 0 | No, el nombre no es visible |
| Declarar let en el nivel más externo | Se conserva | Sí, desde cualquier línea |
| Declarar let en una función externa y devolver una función interna | Sigue desde el valor anterior en cada llamada a la función devuelta | Solo cambia a través de la función devuelta |
Crear funciones con un valor de configuración fijado — fábricas de funciones
Supón que la tasa de puntos cambia según el nivel de membresía. Si pasas la tasa como argumento en cada cálculo, quien llama puede confundir los niveles, y terminas escribiendo la misma tasa una y otra vez. Aquí conviene fijar la tasa una sola vez, de antemano.
Con una fábrica de funciones (una función que recibe un valor de configuración y devuelve una función que lo recuerda), pasas la tasa una sola vez. Un closure también recuerda los argumentos, así que cada función devuelta sigue leyendo el rate con el que se creó.
function makePointCalculator(rate) {
// rate es un argumento. La función devuelta lo sigue recordando
return (amount) => Math.floor(amount * rate);
}
const regularPoint = makePointCalculator(0.01); // Miembros regulares: 1%
const goldPoint = makePointCalculator(0.05); // Miembros Gold: 5%
const platinumPoint = makePointCalculator(0.1); // Miembros Platinum: 10%
console.log(regularPoint(3000)); // 30
console.log(goldPoint(3000)); // 150
console.log(platinumPoint(3000)); // 300
// Sin guardarla en una variable, calcula en el momento con dos pares de paréntesis
console.log(makePointCalculator(0.05)(5000)); // 250
rate que recuerda cada una. Cada función sigue leyendo el rate que le pasaste, a menos que algo lo reasigne.En makePointCalculator(0.05)(5000), el primer par de paréntesis crea una función que recuerda la tasa, y el segundo le pasa el importe y calcula en el momento. Con un solo par de paréntesis, lo que obtienes es una función, no un número calculado.
Una variable nueva cada vez — fábricas y let
Supón que quieres ir descontando el stock restante de cada producto. Si preparas una sola variable compartida para las restas, al reservar camisetas también bajan las gorras que quedan. Cada producto necesita su propio valor.
Igual que con el makeDownloadCounter del principio de este artículo, si declaras un let dentro de una función que devuelve una función, esa variable se crea de nuevo cada vez que llamas a la función externa. Las funciones devueltas no comparten variables, así que, aunque salgan de la misma definición, cada una lee y modifica solo la suya.
function makeStockReserver(stock) {
let rest = stock; // Se crea de nuevo en cada llamada
return (count) => {
rest = rest - count;
return rest;
};
}
const reserveShirt = makeStockReserver(10);
const reserveCap = makeStockReserver(4);
console.log(reserveShirt(3)); // 7
console.log(reserveCap(1)); // 3
console.log(reserveShirt(2)); // 5 (solo baja el stock de camisetas)
console.log(reserveCap(1)); // 2
rest. Bajar uno no toca el stock restante del otro.reserveShirt(2), en el tercer console.log, solo baja el stock de camisetas de 7 a 5, y el de gorras se queda en 3. El siguiente reserveCap(1) devuelve 2, lo que muestra que la función de las gorras también conserva su propio conteo.
Si llamas a la fábrica cada vez, el conteo vuelve a empezar
Si escribes makeStockReserver(10)(3) dos veces, las dos devuelven 7, porque cada llamada crea un rest distinto que empieza en 10. Para que el stock ya descontado pase a la siguiente reserva, guarda la función devuelta en una variable como reserveShirt y llama a esa variable.
Crear funciones en un bucle — un let distinto en cada vuelta
Supón que quieres preparar varias funciones de salida, una por cada nivel de log. Si creas funciones dentro de un bucle y las guardas en un array, las llamas después de que el bucle terminó. Que cada función recuerde o no el valor de su propia vuelta depende de cómo declaraste la variable del bucle.
Este curso no usa var, pero ponerlo al lado de let deja clara la diferencia. Con let en los paréntesis del for, se crea una variable nueva en cada vuelta, y la función creada en esa vuelta recuerda esa variable. var comparte una sola variable entre todas las vueltas, así que todas las funciones leen el valor que quedó al terminar el bucle.
const levels = ["info", "warn", "error"];
// var comparte una sola variable entre todas las vueltas
const varLoggers = [];
for (var i = 0; i < levels.length; i++) {
varLoggers.push(() => `[${levels[i]}]`);
}
console.log(varLoggers[0]()); // [undefined]
// let crea una variable nueva en cada vuelta
const letLoggers = [];
for (let j = 0; j < levels.length; j++) {
letLoggers.push(() => `[${levels[j]}]`);
}
console.log(letLoggers[0]()); // [info]
console.log(letLoggers[2]()); // [error]
var, las tres funciones leen la misma variable; del lado de let, cada una lee el valor de su propia vuelta. Lo que recuerda una función depende de si se crea una variable nueva en cada vuelta.Del lado de var sale [undefined] porque, al salir del bucle, i vale 3 y levels[3] no existe. Del lado de let, levels[j] también se lee en el momento de la llamada, pero el j que se lee es la variable creada en esa vuelta, así que el valor de cada vuelta sigue ahí.
Un closure no recuerda el valor del momento en que se creó
Lo que recuerda un closure es la variable misma, y lee el valor que tenga en el momento de la llamada. Si creas una función que lee let rate = 0.05; y después reasignas rate = 0.1;, esa función también calcula con 0.1. Si quieres fijar un valor de configuración, pásalo como argumento de una fábrica, como en makePointCalculator(0.05).
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2reserveCap empieza con 4 unidades en stock. Después de que reserveShirt descuenta 3, le pasas 1 a reserveCap. ¿Qué devuelve?
Pregunta 3Si llamas a una función creada dentro de for (var i = 0; ...) después de que el bucle terminó, ¿cuánto vale i?