Guía de cierres de JavaScript

En este tutorial, veremos cómo funcionan los ámbitos en JavaScript, así como qué son los cierres y cómo funcionan. Luego repasaremos ejemplos y casos de uso de cierres.

Introducción

Los cierres son un concepto algo abstracto del lenguaje JavaScript y se cuelan en el lado del compilador de la programación. Sin embargo, comprender cómo JavaScript interpreta las funciones, las funciones anidadas, los ámbitos y los entornos léxicos es imprescindible para aprovechar todo su potencial.

En este artículo, intentaremos desmitificar dichos conceptos y proporcionar una guía simple para los cierres de JavaScript.

¿Qué es un cierre?

Primero echemos un vistazo a la definición oficial de cierre de MDN:

Un cierre es la combinación de una función agrupada (encerrada) con referencias a su estado circundante (el entorno léxico). En otras palabras, un cierre le da acceso al alcance de una función externa desde una función interna.

En términos más simples, un cierre es una función que tiene acceso al alcance de una función externa. Para entender esto, echemos un vistazo a cómo funcionan los ámbitos en JavaScript.

Alcance en JavaScript

Alcance determina qué variables son visibles o se puede hacer referencia a ellas en un contexto determinado. El alcance se divide ampliamente en dos tipos: Alcance global y Alcance local:

  • Ámbito global: variables definidas fuera de una función. Se puede acceder a las variables en este ámbito y modificarlas desde cualquier parte del programa, de ahí el nombre "global".

  • Ámbito local: variables definidas dentro de una función. Estas variables son específicas de la función en la que están definidas, por lo que se denominan "locales".

Echemos un vistazo a una variable global y local en JavaScript:

1
2
3
4
5
6
let name = "Joe";

function hello(){
    let message = "Hello";
    console.log(message + " " +name);
}

En el ejemplo anterior, el alcance de name es global, es decir, se puede acceder a él desde cualquier lugar. Por otro lado, message se define dentro de una función, su alcance es local a la función hello().

JavaScript utiliza Ámbito léxico cuando se trata de ámbitos de funciones. Lo que significa que el alcance de una variable se define por la posición de su definición en el código fuente. Esto nos permite hacer referencia a variables globales dentro de ámbitos más pequeños. Una variable local puede usar una variable global, pero viceversa no es posible.

En

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function outer(){
    let x = 10;
    
    function inner() {
        let y = 20;
        console.log(x);
    }
    
    inner();
    console.log(y)
}

outer();

Este código da como resultado:

1
2
10
error: Uncaught ReferenceError: y is not defined

La función inner() puede hacer referencia a x ya que está definida en la función outer(). Sin embargo, la instrucción console.log(y) en la función outer() no puede hacer referencia a la variable y porque está definida en el alcance de la función inner().

Además, en este escenario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let x = 10;

function func1(){
   console.log(x);
}

function func2() {
  let x = 20;
  func1();
}

func2();

La salida será:

1
10

Cuando llamamos a func1() desde dentro de func2(), tenemos una variable de alcance local x. Sin embargo, esta variable es totalmente irrelevante para func1() ya que no es accesible en func1().

Por lo tanto, func1() verifica si hay una variable global con ese identificador disponible y la usa, dando como resultado el valor de 10.

Cierres bajo el capó

Un cierre es una función que tiene acceso a las variables de sus padres incluso después de que la función externa haya regresado. En otras palabras, un cierre tiene tres alcances:

  • Alcance Local - Acceso a variables en su propio alcance
  • Alcance de la función padre - Acceso a variables dentro de su padre
  • Alcance Global - Acceso a variables globales

Echemos un vistazo a un cierre en el trabajo, haciendo una función que devuelve otra función:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function outer() {
    let x = 3
    return function inner(y) {
        return x*y
    }
}

let multiplyByThree = outer();

console.log(multiplyByThree(2));

Esto resulta en:

1
6

Si hacemos un:

1
console.log(multiplyByThree);

Nos reciben con:

1
function inner(y) { return x * y; }

Repasemos el código paso a paso para ver qué sucede debajo del capó:

  1. La función outer() se define en el ámbito global.
  2. Se invoca outer() y devuelve una función que está asignada a multiplyByThree.
    1. New execution context is created for outer().
      • Variable x is set to 3.
    2. Returns a function named inner().
    3. The reference to inner() is assigned to multiplyByThree.
    4. As the outer function finishes execution, all the variables within its scope are deleted.
  3. El resultado de la llamada a la función multiplyByThree(2) se registra en la consola.
    1. inner() is invoked with 2 as the argument. So, y is set to 2.
    2. As inner() preserves the scope chain of its parent function, at the time of execution it will still have access to the value of x.
    3. It returns 6 which gets logged to the console.

En conclusión, incluso después de que la función externa deja de existir, la función interna tiene acceso a las variables definidas en el alcance de la función externa.

Visualización de cierres {#visualización de cierres}

Los cierres se pueden visualizar a través de la consola del desarrollador:

1
2
3
4
5
6
7
8
9
function outer() {
    let x = 3
    return function inner(y) {
        return x*y
    }
}

let multiplyByThree = outside();
console.dir(multiplyByThree);

Al ejecutar el código anterior en la consola del desarrollador, podemos ver que tenemos acceso al contexto de inner(y). Tras una inspección más cercana, podemos ver que parte de su contexto es una matriz [[Ámbitos]], que contiene los tres ámbitos de los que hablábamos.

He aquí que la matriz de ámbitos contiene el ámbito de su función principal, que contiene x = 3:

Elemento de cierre en la consola del desarrollador

Casos de uso común

Los cierres son útiles porque nos ayudan a agrupar datos con funciones que operan sobre esos datos. Esto puede sonar familiar para algunos de ustedes que están familiarizados con la Programación Orientada a Objetos (POO). Como resultado, podemos usar cierres en cualquier lugar donde podamos usar un objeto.

Otro caso de uso importante de los cierres es cuando necesitamos que nuestras variables sean privadas, ya que las variables definidas en el alcance de un cierre están fuera del alcance de las funciones fuera de él. Al mismo tiempo, los cierres tienen acceso a variables en su cadena de alcance.

Veamos el siguiente ejemplo para entender esto mejor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const balance = (function() {
    let privateBalance = 0;

    return {
        increment: function(value){
            privateBalance += value;
            return privateBalance;
        },
        decrement: function(value){
            privateBalance -= value;
            return privateBalance;
        },
        show: function(){
            return privateBalance;
        }
    }
})()

console.log(balance.show()); // 0
console.log(balance.increment(500)); // 500
console.log(balance.decrement(200)); // 300

En este ejemplo, hemos definido una variable constante “saldo” y la hemos establecido como el valor de retorno de nuestra función anónima. Tenga en cuenta que privateBalance solo se puede cambiar llamando a los métodos en saldo.

Conclusión

Aunque los cierres son un concepto bastante especializado en JavaScript, son una herramienta importante en el conjunto de herramientas de un buen desarrollador de JavaScript. Se pueden utilizar para implementar con elegancia soluciones que, de otro modo, serían una tarea difícil.

En este artículo, primero hemos aprendido un poco sobre los ámbitos y cómo se implementan en JavaScript. Luego usamos este conocimiento para comprender cómo funcionan los cierres debajo del capó y cómo usarlos.