Aprende leyendo en orden

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
La variable que recuerda la función devuelta
Nivel más externo (fuera de cualquier llave)
  • const countDownload = makeDownloadCounter() — guarda la función devuelta
  • console.log(total) — el nombre no es visible, así que da ReferenceError
Llaves de makeDownloadCounter
  • let total = 0 — llamar a countDownload() no lo devuelve a 0
  • return countUp — devuelve la función misma, no un valor
Llaves de countUp
  • 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 conteoValor entre llamadas¿Lo pueden sobrescribir líneas de afuera?
Declarar let dentro de la función que cuentaDesaparece al terminar la llamada; la siguiente empieza en 0No, el nombre no es visible
Declarar let en el nivel más externoSe conservaSí, desde cualquier línea
Declarar let en una función externa y devolver una función internaSigue desde el valor anterior en cada llamada a la función devueltaSolo cambia a través de la función devuelta

La prueba gratuita de un video permite hasta tres reproducciones. Declara tú mismo las variables.

① Escribe makeTrialCounter, que devuelve una función que empieza con 3 reproducciones restantes, resta 1 en cada llamada y devuelve cuántas quedan. No debe bajar de 0.

② Crea con makeTrialCounter la función para la prueba.

③ Llama cuatro veces a la función de ② y muestra cada resultado con el formato "Reproducciones restantes: N".

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
Tres funciones a partir de una sola definición
Una definición:makePointCalculatorPasa (0.01)→ regularPointPasa (0.05)→ goldPointPasa (0.1)→ platinumPointrate recordado:0.01rate recordado:0.05rate recordado:0.1Con 3000 yenesdevuelve 30Con 3000 yenesdevuelve 150Con 3000 yenesdevuelve 300
Todas las funciones tienen la misma expresión en el cuerpo; lo único que cambia es el 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.

La tienda principal y la sucursal tienen distintos montos mínimos para el envío gratis y distintos costos de envío. cartTotals ya está declarado.

① Escribe makeShippingFee, que recibe el monto mínimo para el envío gratis y el costo de envío, y devuelve una función que recibe el total del carrito y devuelve el costo de envío.

② Crea las funciones de la tienda principal (gratis desde 4000 yenes; si no, 500 yenes) y de la sucursal (gratis desde 3000 yenes; si no, 350 yenes).

③ Pasa cada importe de cartTotals por las dos y muestra "N yenes → Tienda principal N yenes / Sucursal N yenes".

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
Una variable nueva en cada llamada
makeStockReserver(10) se ejecutaCrea una variablerest = 10makeStockReserver(4) se ejecutaCrea otra variablerest = 4 apartereserveShirt(3) yreserveCap(1)Devuelve 7 y 3sin afectarse
Aunque salgan de la misma definición, cada función tiene su propio 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.

Genera IDs consecutivos para pedidos y devoluciones, cada uno con su propia numeración. Declara tú mismo las variables.

① Escribe makeIdIssuer, que recibe un prefijo y devuelve una función que, en cada llamada, devuelve IDs consecutivos: "prefijo-1", "prefijo-2", etc.

② Crea una función generadora para ORD y otra para RTN.

③ Genera dos IDs de pedido y uno de devolución, y muestra cada uno.

④ Genera un ID de pedido más, muéstralo y comprueba que la numeración sigue donde se quedó.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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]
Qué terminan leyendo las funciones que recuerdan la variable del bucle
var en losparéntesis del forUna sola variablepara las vueltasLas 3 funcionesleen el mismo iDevuelve[undefined]let en losparéntesis del forUna variablenueva por vueltaCada una recuerda0, 1 o 2Devuelve[info]
Del lado de 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).

Crea, para cada método de pago, una función que muestre un aviso de confirmación. methods, labels y notifiers ya están declarados.

① Recorre methods con un for que tenga let en los paréntesis.

② En cada vuelta, usa el nombre del método de pago como clave y guarda en notifiers una función que recibe un número de pedido y muestra un aviso como "[Transferencia bancaria] Pedido A-1002 recibido".

③ Después del bucle, pasa A-1002 a bank y A-1003 a cod.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Si llamas dos veces a countDownload() y luego muestras total en el nivel más externo, ¿qué pasa?

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?