JavaScript

JavaScript

Tutorial JavaScript: Funciones cierre (closure)

JavaScript closures: definición y ejemplos. Aprende a usar closures en JavaScript con ejemplos prácticos y detallados.

Aprende JavaScript y certifícate

Fundamentos de closures: sintaxis, diferencia entre scope y closure

En JavaScript, un closure (cierre) es una función que recuerda el entorno léxico en el que fue creada, incluso después de que dicho entorno haya dejado de existir. Esto significa que una función con closure tiene acceso a las variables de su scope (ámbito) exterior, preservando su estado a través del tiempo.

Para comprender los closures, es esencial recordar primero el concepto de scope. El scope determina la accesibilidad de las variables y funciones en diferentes partes del código. En JavaScript, existen principalmente dos tipos de scope: global y local. Las variables declaradas fuera de cualquier función tienen un scope global y son accesibles desde cualquier parte del código. Por otro lado, las variables declaradas dentro de una función tienen un scope local y solo son accesibles dentro de esa función.

Un closure se genera cuando una función interna accede a variables de una función externa y las mantiene en memoria, incluso después de que la función externa haya terminado de ejecutarse. Esto es posible porque en JavaScript, las funciones son ciudadanos de primera clase, lo que permite que se traten como cualquier otra variable: pueden asignarse a variables, pasarse como argumentos o retornarse desde otras funciones.

Aquí tienes un ejemplo que ilustra este concepto:

function crearContador() {
  let contador = 0;
  
  function incrementar() {
    contador += 1;
    console.log(contador);
  }
  
  return incrementar;
}

const contador = crearContador();
contador(); // Imprime 1
contador(); // Imprime 2
contador(); // Imprime 3

En este ejemplo, la función crearContador define una variable local contador y una función interna incrementar que incrementa y muestra el valor de contador. Al retornar incrementar, estamos creando un closure que mantiene una referencia a contador. Aunque crearContador ha finalizado su ejecución, la función incrementar sigue teniendo acceso a contador gracias al closure.

La diferencia entre scope y closure radica en que el scope es el contexto actual de ejecución, que define qué variables y funciones están disponibles en un punto dado del código. Un closure, por su parte, es una función que captura variables de su scope exterior y las mantiene accesibles, incluso después de que dicho scope haya terminado.

Los closures son fundamentales para crear funciones privadas y encapsular datos. Permiten que una función recuerde el estado de su entorno, lo que es especialmente útil en programación asincrónica y en la creación de módulos. Sin embargo, es importante usarlos con cuidado, ya que un mal manejo puede llevar a fugas de memoria si las variables capturadas nunca se liberan.

En resumen, mientras que el scope define el alcance de las variables en diferentes partes del código, los closures permiten que una función acceda y retenga variables de un scope externo, proporcionando una poderosa herramienta para la abstracción y el encapsulamiento en JavaScript.

Implementación de closures

La implementación de closures en JavaScript aprovecha la capacidad de las funciones para acceder al contexto en el que fueron creadas. Para crear un closure, es necesario definir una función dentro de otra función y permitir que la función interna acceda a las variables de la función externa.

Considera el siguiente ejemplo básico:

function greeting(name) {
  return function() {
    console.log(`Hello, ${name}`);
  };
}

const greetJohn = greeting('John');
greetJohn(); // Imprime "Hello, John"

En este ejemplo, la función externa greeting recibe un parámetro name y retorna una función interna anónima. La función interna accede al parámetro name de la función externa, creando así un closure. Cuando invocamos greetJohn(), la función interna recuerda el valor de name aunque la ejecución de greeting haya finalizado.

Es importante destacar que los closures no solo pueden acceder a variables, sino también modificarlas. Veamos un ejemplo donde una variable es incrementada a través de un closure:

function counter() {
  let count = 0;
  return function() {
    count++;
    console.log(count);
  };
}

const increment = counter();
increment(); // Imprime 1
increment(); // Imprime 2
increment(); // Imprime 3

Aquí, la variable count es privada para la función counter y solo puede ser modificada a través del closure retornado. Esto permite encapsular el estado y prevenir accesos directos desde el exterior.

Los closures también pueden aceptar parámetros, lo que los hace aún más versátiles:

function multiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = multiplier(2);
console.log(double(5)); // Imprime 10

const triple = multiplier(3);
console.log(triple(5)); // Imprime 15

En este caso, la función multiplier crea un closure que retiene el valor de factor. Las funciones double y triple son closures que recuerdan el valor con el que fueron creadas y lo utilizan al ser llamadas.

Para una implementación más elaborada, podemos usar closures para crear métodos en objetos:

function createPerson(name) {
  let age = 0;
  return {
    incrementAge: function() {
      age++;
      console.log(`${name} is ${age} years old`);
    }
  };
}

const person = createPerson('Anna');
person.incrementAge(); // Imprime "Anna is 1 years old"
person.incrementAge(); // Imprime "Anna is 2 years old"

En este ejemplo, la variable age es privada y solo accesible a través del método incrementAge. Esto es posible gracias al closure que mantiene la referencia a age.

Es importante entender que los closures pueden tener implicaciones en el uso de memoria. Como las variables en el scope exterior permanecen en memoria mientras existan referencias a los closures, es posible que se produzcan fugas de memoria si no se manejan adecuadamente. Por ello, es recomendable liberar las referencias a closures cuando ya no sean necesarias.

Además, al trabajar con loops y closures, es común encontrarse con comportamientos inesperados debido a variables compartidas. Observa este ejemplo erróneo:

for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// Imprime 4 tres veces

Este código intenta imprimir los valores de i en cada iteración, pero debido al alcance de var, el closure captura la referencia a la misma variable i, que al final del loop es 4. Para solucionar esto, podemos emplear let o crear un closure adicional:

for (let i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// Imprime 1, 2 y 3 con un segundo de retraso

Al usar let, creamos una nueva variable i en cada iteración, y el closure captura el valor correcto. De este modo, evitamos el problema de la variable compartida y el closure funciona como se espera.

Comprender la implementación de closures en JavaScript es fundamental para escribir código más modular y mantener la integridad de las variables en nuestros programas. Los closures nos permiten crear funciones con estado y controlar el acceso a variables, mejorando así la abstracción y el diseño de nuestras aplicaciones.

Patrones comunes con closures: Encapsulación y datos privados, Funciones de fábrica (factory functions)

Los closures son fundamentales en JavaScript para implementar patrones de diseño que permiten una mejor organización y seguridad del código. Entre estos patrones, destacan la encapsulación de datos y el uso de funciones de fábrica (factory functions).

Encapsulación y datos privados

La encapsulación es un principio de programación que permite ocultar detalles internos de una función o objeto, exponiendo solo una interfaz pública para interactuar con ellos. En JavaScript, los closures facilitan la creación de datos privados, permitiendo que variables internas sean inaccesibles desde el exterior, asegurando así la integridad de los datos.

Considere el siguiente ejemplo:

function createCounter() {
  let count = 0;

  return {
    increment() {
      count++;
      console.log(`Count is ${count}`);
    },
    decrement() {
      count--;
      console.log(`Count is ${count}`);
    }
  };
}

const counter = createCounter();
counter.increment(); // Imprime "Count is 1"
counter.increment(); // Imprime "Count is 2"
counter.decrement(); // Imprime "Count is 1"

En este caso, la variable count es privada y no es accesible desde fuera de la función createCounter. Los métodos increment y decrement tienen acceso a count gracias al closure, pero el código externo no puede modificar count directamente. Así, se consigue proteger el estado interno y controlar cómo se modifica.

Intentar acceder a count desde fuera resultará en un error:

console.log(counter.count); // Imprime "undefined"

Esta capacidad de mantener datos privados es esencial para crear módulos o componentes más seguros y confiables.

Otro ejemplo es crear un almacén de datos con métodos para manipular la información:

function createPerson(name) {
  let _name = name;

  return {
    getName() {
      return _name;
    },
    setName(newName) {
      _name = newName;
    }
  };
}

const person = createPerson('Alice');
console.log(person.getName()); // Imprime "Alice"
person.setName('Bob');
console.log(person.getName()); // Imprime "Bob"

Aquí, la propiedad _name es privada y solo puede ser accedida o modificada mediante los métodos getName y setName. Esto permite controlar cómo se interactúa con los datos internos, evitando modificaciones indebidas.

Funciones de fábrica (factory functions)

Las funciones de fábrica son funciones que crean y retornan objetos. Utilizan closures para encapsular datos y comportamientos, ofreciendo una alternativa a las clases y la herencia clásica. Este patrón es especialmente útil para crear múltiples instancias con comportamientos similares pero datos independientes.

Un ejemplo básico de una función de fábrica es:

function createUser(username) {
  return {
    getUsername() {
      return username;
    }
  };
}

const user1 = createUser('user_one');
const user2 = createUser('user_two');

console.log(user1.getUsername()); // Imprime "user_one"
console.log(user2.getUsername()); // Imprime "user_two"

En este caso, createUser es una función de fábrica que genera objetos con un método getUsername. La variable username queda encapsulada y es única para cada instancia creada.

Las funciones de fábrica pueden también incluir métodos compartidos y datos privados más complejos:

function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      balance += amount;
      console.log(`Deposited ${amount}. New balance is ${balance}.`);
    },
    withdraw(amount) {
      if (amount > balance) {
        console.log('Insufficient funds.');
      } else {
        balance -= amount;
        console.log(`Withdrew ${amount}. New balance is ${balance}.`);
      }
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(1000);
account.deposit(500);   // Imprime "Deposited 500. New balance is 1500."
account.withdraw(2000); // Imprime "Insufficient funds."
account.withdraw(300);  // Imprime "Withdrew 300. New balance is 1200."
console.log(account.getBalance()); // Imprime 1200

Aquí, el balance de la cuenta es un dato privado y solo puede ser modificado a través de los métodos proporcionados. Esto asegura que el estado de la cuenta se mantenga consistente y controlado.

Ventajas de las funciones de fábrica

Al utilizar funciones de fábrica, se obtienen varias ventajas:

Encapsulación: Los datos internos están protegidos y solo accesibles mediante la interfaz pública.

Flexibilidad: Es fácil crear objetos personalizados sin la complejidad de las clases y la herencia.

Composición sobre herencia: Permite combinar funcionalidades reutilizables en lugar de depender de cadenas de herencia.

Simplicidad en el uso de closures: Aprovecha los closures de manera natural para mantener el estado interno.

Comparación con clases

Aunque las clases de ES6 también permiten crear objetos con datos privados (usando campos privados), las funciones de fábrica ofrecen una alternativa más funcional y a veces más sencilla. Además, las funciones de fábrica pueden ser más flexibles en ciertos casos, ya que no requieren el uso de new y pueden aprovechar las características de las funciones superiores.

Por ejemplo, es posible combinar funciones de fábrica para crear objetos más complejos:

function withLogging(obj) {
  return {
    ...obj,
    log(message) {
      console.log(`[${new Date().toISOString()}] ${message}`);
    }
  };
}

function createProduct(name, price) {
  return withLogging({
    name,
    price,
    display() {
      this.log(`Product: ${this.name}, Price: ${this.price}`);
    }
  });
}

const product = createProduct('Laptop', 1500);
product.display(); // Imprime el log con la información del producto

En este ejemplo, se combina la función withLogging con createProduct para añadir funcionalidades adicionales al objeto resultante. Esto demuestra la composición de funciones y objetos utilizando funciones de fábrica y closures.

Uso en programación funcional

Las funciones de fábrica y los closures encajan perfectamente en el paradigma de la programación funcional, donde las funciones son ciudadanos de primera clase y se promueve la inmutabilidad y las funciones puras. Aunque los closures permiten mantener estado interno, su uso controlado puede integrarse en aplicaciones funcionales para gestionar datos y comportamientos.

Por ejemplo, se pueden crear funciones generadoras de operaciones:

function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // Imprime 10
console.log(triple(5)); // Imprime 15

Aquí, createMultiplier es una función de fábrica que genera nuevas funciones utilizando closures. Estas funciones pueden ser utilizadas como herramientas en transformaciones funcionales.

En resumen, los closures y las funciones de fábrica son herramientas poderosas en JavaScript que permiten implementar patrones como la encapsulación y el manejo de datos privados. Estos patrones contribuyen a escribir código más seguro, modular y mantenible, aprovechando las características únicas del lenguaje.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende JavaScript online

Ejercicios de esta lección Funciones cierre (closure)

Evalúa tus conocimientos de esta lección Funciones cierre (closure) con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Funciones flecha

JavaScript
Puzzle

Polimorfismo

JavaScript
Test

Array

JavaScript
Código

Transformación con map()

JavaScript
Test

Gestor de tareas con JavaScript

JavaScript
Proyecto

Manipulación DOM

JavaScript
Test

Funciones

JavaScript
Test

Funciones flecha

JavaScript
Código

Async / Await

JavaScript
Código

Creación y uso de variables

JavaScript
Test

Excepciones

JavaScript
Puzzle

Promises

JavaScript
Código

Funciones cierre (closure)

JavaScript
Test

Herencia

JavaScript
Puzzle

Herencia

JavaScript
Test

Estructuras de control

JavaScript
Código

Selección de elementos DOM

JavaScript
Test

Modificación de elementos DOM

JavaScript
Test

Filtrado con filter() y find()

JavaScript
Test

Funciones cierre (closure)

JavaScript
Puzzle

Funciones

JavaScript
Puzzle

Mapas con Map

JavaScript
Test

Reducción con reduce()

JavaScript
Test

Callbacks

JavaScript
Puzzle

Manipulación DOM

JavaScript
Puzzle

Promises

JavaScript
Test

Async / Await

JavaScript
Test

Eventos del DOM

JavaScript
Puzzle

Async / Await

JavaScript
Puzzle

Promises

JavaScript
Puzzle

Filtrado con filter() y find()

JavaScript
Código

Callbacks

JavaScript
Test

Creación de clases y objetos Restaurante

JavaScript
Código

Reducción con reduce()

JavaScript
Código

Filtrado con filter() y find()

JavaScript
Puzzle

Reducción con reduce()

JavaScript
Puzzle

Conjuntos con Set

JavaScript
Puzzle

Herencia de clases

JavaScript
Código

Eventos del DOM

JavaScript
Test

Clases y objetos

JavaScript
Puzzle

Modificación de elementos DOM

JavaScript
Puzzle

Mapas con Map

JavaScript
Puzzle

Introducción a JavaScript

JavaScript
Test

Funciones

JavaScript
Código

Tipos de datos

JavaScript
Test

Clases y objetos

JavaScript
Test

Array

JavaScript
Test

Conjuntos con Set

JavaScript
Test

Array

JavaScript
Puzzle

Encapsulación

JavaScript
Puzzle

Clases y objetos

JavaScript
Código

Uso de operadores

JavaScript
Puzzle

Uso de operadores

JavaScript
Test

Estructuras de control

JavaScript
Test

Excepciones

JavaScript
Test

Transformación con map()

JavaScript
Puzzle

Funciones flecha

JavaScript
Test

Selección de elementos DOM

JavaScript
Puzzle

Encapsulación

JavaScript
Test

Mapas con Map

JavaScript
Código

Creación y uso de variables

JavaScript
Puzzle

Polimorfismo

JavaScript
Puzzle

Tipos de datos

JavaScript
Puzzle

Estructuras de control

JavaScript
Puzzle

Todas las lecciones de JavaScript

Accede a todas las lecciones de JavaScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a JavaScript y certifícate

Certificados de superación de JavaScript

Supera todos los ejercicios de programación del curso de JavaScript y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  1. Comprender el concepto de closures y su importancia en JavaScript.
  2. Conocer cómo se crea un closure y cómo retiene el acceso a su ámbito léxico original.
  3. Aprender a utilizar closures para crear funciones con contextos específicos y mantener la privacidad de ciertas variables.
  4. Entender cómo los closures pueden ser útiles para preservar el estado entre llamadas a funciones y permitir una programación más modular y reutilizable.