Entender a la perfección de closures es un paso importante para convertirse en un desarrollador JavaScript.

Hay una razón por la qué puede ser difícil entender los closures: por lo general, se enseña al revés. Puede que te hayan enseñado qué es un closure, pero es posible que no hayas entendido el porqué son útiles para el desarrollador promedio, o dentro de tu propio código.

Entonces, ¿Por qué los closures importan en nuestro código JavaScript del día a día?

En lugar de ver a los closures como un tema a memorizar para algún tipo de examen, veamos que pasos nos pueden llevar a ver un closure en primer lugar. Una vez veamos lo qué son, descubriremos porqué los closures valen la pena en tu código JavaScript.

¿Haz visto un closure en la vida real?

Dígamos que estamos haciendo un clon del sitio de blogging Medium y queremos que cada usuario sea capaz de dar me gusta a diferentes posts.

Cada vez que un usuario haga click en el botón me gusta, su valor será incrementado por cada vez.

Piensa en el botón aplaudir de Medium:

https://raw.githubusercontent.com/kikobeats/react-clap-button/HEAD/demo.gif

La función handleLikePost manejará el incremento del contador, cada vez que se le de clic al botón, y la variable likeCount mantendrá el número de me gusta registrados:

// global scope
let likeCount = 0;

function handleLikePost() {
  // function scope
  likeCount = likeCount + 1;
}

handleLikePost();
console.log("like count:", likeCount); // like count: 1

Cuando un usuario haga clic en me gusta en un post, llamaremos a la función handleLikePost y esto incrementará nuestra variable likeCount en 1.

Esto funciona porque sabemos que esa función puede acceder a las variables fuera de sí mismas.

En otras palabras, las funciones pueden acceder cualquier variable definida en cualquier ámbito (scope) superior.

Sin embargo, hay un problema con este código. Ya que likeCount está en el ámbito global, y no en alguna función, likeCount es una variable global. Las variables globales pueden ser usadas (y actualizadas) por cualquier otra pieza de código o función en nuestra aplicación.

Por ejemplo, ¿Qué pasa si, erróneamente, después de nuestra función fijamos nuestra variable likeCount a cero?

let likeCount = 0;

function handleLikePost() {
  likeCount = likeCount + 1;
}

handleLikePost();
likeCount = 0;
console.log("like count:", likeCount); // like count: 0

Naturalmente, likeCount será siempre cero.

Cuando únicamente una función necesita una pieza de datos, ese dato solo necesita existir locamente, esto es, dentro de esa función.

Ahora traigamos likeCount dentro de nuestra función:

function handleLikePost() {
  // likeCount moved from global scope to function scope
  let likeCount = 0;
  likeCount = likeCount + 1;
}

Fíjate que hay una manera más corta de escribir la línea donde incrementamos likeCount. En lugar de decir que likeCount es igual al valor actual (en ese momento) de likeCount y agregar 1, podemos simplemente usar el operador +=, así:

function handleLikePost() {
  let likeCount = 0;
  likeCount += 1;
}

Y para que obtengamos el mismo resultado en consola, debemos también traernos nuestro console.log dentro de la función.

function handleLikePost() {
  let likeCount = 0;
  likeCount += 1;
  console.log("like count:", likeCount);
}

handleLikePost(); // like count: 1

Ahora vemos lo mismo que antes.

Ahora los usuarios deberían ser capaces de darle me gusta a un post las veces que ellos quieran, llamemos a la función handleLikePost unas cuantas veces más:

handleLikePost(); // like count: 1
handleLikePost(); // like count: 1
handleLikePost(); // like count: 1

Sin embargo, cuando ejecutamos este código, hay un problema.

Esperaríamos ver a la variable likeCount seguir incrementando, pero solo vemos un 1 cada vez. ¿Por qué pasa esto?

Detente un momento, mira nuestro código e intenta explicar porqué nuestra variable likeCount no se está incrementado cómo antes.

Veamos al código de la función handleLikePost:

function handleLikePost() {
  let likeCount = 0;
  likeCount += 1;
  console.log("like count:", likeCount);
}

Cada vez que lo usamos, estamos recreando la variable likeCount, la cual es inicializada a cero.

¡No hay duda del porqué no podemos mantener la cuenta entre cada llamada a la función! La variable es constantemente inicializada a cero, luego se incrementa en 1, y después la función finaliza su ejecución.

Estamos atorados. Nuestra variable necesita vivir dentro de la función handleLikePost, pero no podemos mantener la cuenta.

Necesitamos algo que nos permita preservar o recordar el valor de la variable likeCount entre las llamadas a la función.

Qué tal si probamos algo que puede parecer un poco extraño a primera vista: qué tal si intentamos poner una función dentro de nuestra función:

function handleLikePost() {
  let likeCount = 0;
  likeCount += 1;
  function() {

  }
}

handleLikePost();

Vamos a nombrar esta función addLike. ¿La razón? Porque ahora será responsable de incrementa la variable likeCount.

En la mayoría de casos, nuestra nueva función no necesitaría un nombre. Podría ser anónima. La estamos nombrando con el próposito de referinos a ella más fácilmente en nuestra explicación.

La función addLike ahora será responsable de incrementar nuestra variable likeCount, entonces moveremos la línea donde incrementamos la variable en 1 dentro de esta función interna.

function handleLikePost() {
  let likeCount = 0;
  function addLike() {
    likeCount += 1;
  }
}

¿Qué pasa si llamamos a la función addLike dentro de la función handleLikePost?

Lo qué pasará es que la función addLike incrementará la variable likeCount, pero aún así la variable likeCount será destruida. Así que, otra vez, perdemos la cuenta y nuestro resultado es cero.

Pero si en lugar de llamar a la función addLike dentro de su función padre, ¿Qué pasaría si la llamamos por fuera? Esto puede parecer aún más extraño. ¿Y cómo haríamos eso?

A este punto, sabemos que las funciones retornan valores. Por ejemplo, podríamos retornar el valor de la variable likeCount al final de la función handleLikePost para pasarlo a otras partes de nuestro programa:

function handleLikePost() {
  let likeCount = 0;
  function addLike() {
    likeCount += 1;
  }
  addLike();
  return likeCount;
}

Pero en lugar de hacer eso, retornemos la variable likeCount dentro de la función addLike y luego retornemos a la función addLike misma:

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
  // addLike();
}

handleLikePost();

Esto puede verse bizarro, pero es permitido en JavaScript. Podemos tratar a las funciones como variables. Esto significa que una función puede ser retornada por otra función. Al retornar la función interna, podemos llamarla desde afuera de su función padre.

¿Pero cómo hacemos eso? Piensa en esto por un minuto e intenta averiguarlo por tu cuenta...

Primero, para ver mejor lo qué está pasando, vamos a imprimir la llamada a la función handleLikePost con un console.log, asi: console.log(handleLikePost()). Mira que pasa cuando lo ejecutamos:

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
}

console.log(handleLikePost()); // ƒ addLike()

Como era de esperarse, vemos a la función addLike. ¿Por qué? Porque la estamos retornado después de todo.

Para llamarla, ¿Podríamos solo recibirla en otra variable? Como acabamos de decir, las funciones pueden ser tratadas como cualquier otra variable en JavaScript. Si podemos retornar una función desde otra, podemos recibirla en una variable también. Recibamosla en una variable con nombre like:

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
}

const like = handleLikePost();

Y finalmente, llamemos like. Hagámoslo unas cuantas veces e imprimamos cada resultado con un console.log:

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
}

const like = handleLikePost();

console.log(like()); // 1
console.log(like()); // 2
console.log(like()); // 3

¡Nuestra variablelikeCount finalmente mantiene el conteo! Cada vez que nosotros llamamos a la función like, la variable likeCount es incrementada a partir de su valor previo.

¿Pero qué fue lo que paso aquí? Bien, averiguamos cómo llamar a la función addLike desde fuera del ámbito en el cuál fue declarada. Hicimos esto al retornar la función interna desde la función padre y, de esta manera, preservamos el acceso al ámbito de la función padre, la recibimos en la variable like, y al final, la ejecutamos.

¿Cómo funciona un closure, línea por línea?

Sí, esa fue nuestra implementación, claro, ¿Pero cómo hicimos para preservar el valor de la variable likeCount entre cada llamada a la función?

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
}

const like = handleLikePost();

console.log(like()); // 1
  1. Cuando la función padre handleLikePost es ejecutada, crea una instancia de la función interna addLike; esa función tiene acceso directo a la variable likeCount, la cuál está un ámbito más arriba.
  2. Llamamos a la función addLike desde afuera del ámbito en el cuál fue declarada. Hicimos eso al retornar la función interna desde la función padre, así preservamos una referencia a su ámbito, la recibimos en la variables like, y la ejecutamos.
  3. Cuando la función like finaliza su ejecución, normalmente, todas sus variables serían removidas de la memoria por el garbage collector (esto es un proceso automático que el compilador de JavaScript hace). Esperaríamos que cada valor de likeCount se elimine cuando la función termina, pero no pasa esto.

¿Cuál es la razón? El closure.

Ya que las instancias de la función interna aún viven (la función que recibimos en la variable like), el closure aún preserva el valor y el acceso a la variable countLike.

Es normal pensar que declarar una función dentro de otra es como declarar una función en el ámbito global, pero no.

Es por esto que los closures dan a las funciones súper poderes, porque es una propiedad especial que no está presente en en ninguna otra parte del lenguaje.

La vida útil de una variable

Para apreciar mejor a los closures, tenemos que entender cómo JavaScript trata a las variables que son creadas. Puedes haberte preguntado qué le pasa a las variables cuando cierro una página web o abro otra página dentro de la aplicación. ¿Cuánto viven las variables?

Las variables globales viven hasta que el programa es descartado, por ejemplo cuando cierras la ventana. Viven lo que viva la ejecución del programa.

Sin embargo, las variables locales tienen vidas cortas. Son creadas cuando una función es invocada, y eliminadas cuando la función termina su ejecución.

Así que antes, cuando la variable likeCount era solo una variables local, solo vivia mientras la función corría. La variable likeCount era creada al inicio de la ejecución y destruida al final.

Los closures no son capturas instantáneas: mantienen a las variable locales vivas

A veces se dice que los closures en JavaScript son similares a capturas instantáneas, una foto del programa en cierto periodo de tiempo. Esta es una idea equivocada que podemos disipar agregando otra característica a la funcionalidad de nuestro botón me gusta.

Dígamos que en raras ocaciones, queremos permitir a los usuarios que al hacer doble clic en el botón este incremente el valor de la variable likeCount por 2 en lugar de por 1.

¿Cómo agregamos esta característica?

Otra forma de pasar valores a una función es a través de argumentos, los cuales operan justo como variables locales.

Vamos a pasar un argumento nombrado step a la función, el cuál nos permitirá proveer un valor dinámico e intercambiable para incrementar nuestro contador en lugar de un valor fijo 1.

function handleLikePost(step) {
  let likeCount = 0;
  return function addLike() {
    likeCount += step;
    // likeCount += 1;
    return likeCount;
  };
}

Ahora, intentemos hacer una función especial que nos permitirá hacer doble clic a nuestros posts, doubleLike. Pasaremos un 2 como valor al argumento step y luego intentaremos llama a nuestras dos funciones, like and doubleLike:

function handleLikePost(step) {
  let likeCount = 0;
  return function addLike() {
    likeCount += step;
    return likeCount;
  };
}

const like = handleLikePost(1);
const doubleLike = handleLikePost(2);

like(); // 1
like(); // 2

doubleLike(); // 2 (the count is still being preserved!)
doubleLike(); // 4

Vemos que el valor de la variable likeCount se mantiene también para la función doubleLike.

¿Qué está pasando aquí?

Cada instancia de la función interna addLike preserva el acceso a las variables likeCount and step del ámbito de su función padre handleLikePost. La variable step se mantiene viva, y su valor es actualizado por cada invocación a la función interna. Ya qué el acceso al ámbito (el closure) es sobre las variables y no solo una instántanea de sus valores, estas actualizaciones son preservadas entre cada llamada a la función.

Entonces, ¿Todo lo que este código nos muestra es el hecho de que podemos pasar valores dinámicos para cambiar el resultado de nuestra función? No, no es todo. ¡Nos muestra qué las variables locales aún están vivas! Los closures mantienen a las variables locales vivas aún cuando estas debieron ser destruidas tiempo atrás.

En otras palabras, los closures no son estáticos y fijos, como una captura instantánea de los valores de las variables en cierto periodo del tiempo, sino que mantienen a las variables locales vivas y proveen acceso a ellas. Como resultado, podemos usar closures para observar o hacer actualizaciones a estas variables a lo largo del tiempo.

¿Qué es exactamente un closure?

Ya que viste porque los closures son útiles, hay dos condiciones para que exista un closure:

  1. Los closures son una propiedad exclusiva de las funciones.
  2. Para crear un closure, primero debes ejecutar una función en un ámbito diferente a ese en el cuál se definió.

¿Por qué deberíamos saber de closures?

Respondamos a la pregunta original que nos dimos a la tarea de responder. Basado en todo lo que has visto, detente un momento e intenta responderla tu mismo. ¿Por qué nos deberían importan los closures siendo desarrolladores JavaScript?

Los closures son importantes para ti y tu código porque nos permiten recordar valores, lo cuál es una muy poderosa y única característica en el lenguaje que solo las funciones poseen.

Lo vimos justo en este ejemplo. Después de todo, ¿Qué uso tiene una variable contador que no recuerda el número de me gusta? Te encontraras con eso frecuentemente en tu carrera al trabajar con JavaScript. Necesita mantener el valor de una variable pero a la vez separada del resto de variables. ¿Qué usas? Una función. ¿Por qué? Para mantener un valor a través del tiempo en un closure.

Y con eso, estas un paso adelante de otros desarrolladores.

¿Quieres convertirte en un maestro en JavaScript? Únete al JS Bootcamp 2020? (en inglés)

Join the 2020 JS Bootcamp

Follow + Say Hi! ? TwitterInstagramcodeartistry.io

Traducido del artículo de Reed Barger - Why You Should Know JavaScript Closures