Funciones cierre (closure)

Intermedio
JavaScript
JavaScript
Actualizado: 13/05/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

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 crearSaludador(nombre) {
  // La función externa define una variable 'nombre'
  
  function saludar() {
    // La función interna usa la variable 'nombre'
    console.log("¡Hola, " + nombre + "!");
  }
  
  return saludar; // Devolvemos la función interna
}

const saludarAna = crearSaludador("Ana");
const saludarJuan = crearSaludador("Juan");

// Ahora podemos usar estas funciones
saludarAna();  // Muestra: ¡Hola, Ana!
saludarJuan(); // Muestra: ¡Hola, Juan!

Vamos a analizar paso a paso qué ocurre:

  1. Creamos una función crearSaludador que recibe un parámetro nombre
  2. Dentro, definimos otra función saludar que usa el parámetro nombre
  3. La función externa devuelve la función interna
  4. Creamos dos funciones diferentes llamando a crearSaludador con diferentes nombres
  5. Cada función creada "recuerda" el nombre con el que fue creada

Lo sorprendente es que aunque la función crearSaludador ya terminó de ejecutarse, las funciones saludarAna y saludarJuan todavía recuerdan el valor de nombre que existía cuando fueron creadas. ¡Esto es un closure!

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

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.

Aprendizajes de esta lección

  • Comprender el concepto de closures y su importancia en JavaScript.
  • Conocer cómo se crea un closure y cómo retiene el acceso a su ámbito léxico original.
  • Aprender a utilizar closures para crear funciones con contextos específicos y mantener la privacidad de ciertas variables.
  • 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.

Completa JavaScript y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración