Lo que debes de saber de los Closures de JS en 20 minutos

Closure es un concepto complicado…

No porque sea dificíl per se, sino porque es un concepto abastracto que involucra varios conceptos que trabajan entre sí: scopes, bloques, funciones internas, etc.

Sin embargo es un concepto tan común que probablemente ya lo hayas usado antes aunque no te hayas dado cuenta. La única excepción es si apenas estás empezando en este mundo de la programación.

Claro, no lo necesitas entender para sobrevivir ni para lograr solucionar los problemas que se te cruzen. Pero te va a hacer la vida más fácil. Y si quieres dominar Javascript debes de aprenderlo.

No esta de mas decir que lo más seguro es que tengas que releer un montón de veces los ejemplos y las explicaciones. Pero no te preocupes, es normal.


El primer paso para entenderlo es saber para que sirve. ¿Qué se logra con un closure?

Un closure nos sirve para encapsular las variables que necesitamos dentro de un scope manteniendo las referencias para acceder a sus valores. De esta manera se puede acceder a esta variables dentro de esas funciones para un uso futuro.

…Ok…así que guarda algo…¿para que lo uses después? Si, y no esperaba que lo entendieras con solo eso. Pero ve grabandote esto en la mente. Closure es un comportamiento, un comportamiento que solo tienen las funciones.

Ejemplo

Para que tu puedas ver al comportamiento de closure en acción necesitas llamar a una función desde un scope diferente al scope en el que fue declarada. De otra manera no verás ningun cambio perceptible en su comportamiento.

La forma más fácil de observar esto es utilizando funciones internas.

Una función interna como su nombre lo indica es una función que se encuentra declarada dentro de otra función en lugar de a nivel global como comúnmente se hace.

function getMovieFromList(movieList, movieId) {

    const movie = movieList.find(movie => movie.id === movieId)

    function sellMovie(amount) {
        console.log(`Movie "${movie.title}": sold for ${amount}`)
    }

    sellMovie(150)
}

const properties = [
    {
        id: 1,
        title: 'A Nightmare on Elm Street'
    },
    {
        id: 2,
        title: 'Halloween'
    }
]

getMovieFromList(properties, 1)

En este caso se manda a llamar getMovieFromList y dentro de esa función se llama a su vez a sellMovie, la cual nos imprime el siguiente valor en consola.

Movie "A Nightmare on Elm Street": sold for 50

Este es un ejemplo de una función interna que se declara y se manda a llamar dentro del mismo scope. Es decir…justo lo que NO sirve para ver un closure.

Afortunadamente arreglarlo es relativamente simple. Solo hay que hacer que sellMovie sea accesible fuera de ese local scope. Eso se logra regresando la función como parte del valor de retorno.

function getMovieFromList(movieList, movieId) {

    const movie = movieList.find(movie => movie.id === movieId)

    return function sellMovie(amount) {
        console.log(`Movie "${movie.title}": sold for ${amount}`)
    }
}

const properties = [
    {
        id: 1,
        title: 'A Nightmare on Elm Street'
    },
    {
        id: 2,
        title: 'Halloween'
    }
]

getMovieFromList(properties, 1)

Ahora no se imprime nada en consola. La razón es que getMovieFromList regresa una instancia de la función de sellMovie, pero no la manda a llamar en sí. Para mandarla a llamar necesitas hacer unos ajustes extra:

function getMovieFromList(movieList, movieId) {

    const movie = movieList.find(movie => movie.id === movieId)

    return function sellMovie(amount) {
        console.log(`Movie "${movie.title}": sold for ${amount}`)
    }
}

const properties = [
    {
        id: 1,
        title: 'A Nightmare on Elm Street'
    },
    {
        id: 2,
        title: 'Halloween'
    }
]

const movieOne = getMovieFromList(properties, 1)
const movieTwo = getMovieFromList(properties, 2)

movieOne(150)
movieTwo(175)

Al correr el código anterior se obtiene el siguiente resultado:

Movie "A Nightmare on Elm Street": sold for 150
Movie "Halloween": sold for 175

Ahora aquí viene la parte confusa.

Tenemos 2 instancias de la función sellMovie, cada una guardada en las variables movieOne y movieTwo.

Si bien cada instancia de sellMovie recibe un parámetro amount también es evidente que hay algo mas que las hace diferentes. Esto es porque cada una también guarda una referencia a un objeto movie diferente. Este objeto se obtuvo durante las llamadas a getMovieFromList.

movieOne tiene una referencia a una instancia de sellMovie donde la función guarda una referencia a la película de “A Nightmare on Elm Street”. movieTwo por su parte tiene una referencia a otra instancia de sellMovie donde la función guarda una referencia a la película de “Halloween”.

¡Listo! ¡Lo lograste! Ya hiciste un par de closures.

Espera… ¿Que?

Si así es, acabas de ver a los closures en acción. La referencia que existe de la función interna (sellMovie) a una variable en un scope externo (movie) es a lo que se le denomina closure.

En términos prácticos los closures de cada instancia permiten que las funciones mantengan las referencias a los valores de movie correspondientes. De otra manera al perder las referencias el Recolector de Basura quitaría los valores de memoria y haría que las funciones internas fueran prácticamente inservibles.

*Para entender bien la ventaja de este comportamiento es ideal que ya conozcas la forma en la que trabaja el Recolector de Basura en Javascript. Para eso te recomiendo que leas este artículo.


Es un concepto dificíl de aterrizar, no solo por lo abstracto sino también porque el nombre no es muy intuitivo. ¿Por qué se llaman closures?

Puedes verlo de esta manera.

Closure puede traducirse como cierre. Cada instancia de sellMovie se “cierra” sobre las variables exteriores (en este caso movie). Así que puedes imaginarte al local scope de la función lanzando una especie de red que atrapa a las variables externas “encerrandolas” y evitando que se las lleve el Recolector de Basura.

BONUS

Es importante que recuerdes que un closure ayuda a guardar las referencias en memoria no los valores. Un error muy común es confundir este comportamiento e intentar que un closure se comporte como si se guardara el valor.

¿Qué crees que se imprima al correr este script?

var array = [];

for (var i = 0; i < 3; i++) {
    array[i] = function keepI(){
        console.log(i)
    }
}

array[0]()
array[1]()
array[2]()
3
3
3

Si los closures actuaran sobre los valores lo que se imprimiría sería 0,1 y 2. Sin embargo como lo que “encerraron” los closures fue la referencia entonces en cuánto se cambia el valor de la variable i se afecta el resultado para todos ellos.