Les closures són una característica fonamental en JavaScript que permet als desenvolupadors crear funcions més flexibles i potents. En termes simples, una closure és una funció que té accés a variables del seu entorn de definició, fins i tot després que la funció hagi estat cridada i l'entorn hagi desaparegut.
Per seguir correctament aquest article, és recomanable tenir coneixements bàsics de JavaScript i funcions. Explorarem des dels conceptes bàsics fins a exemples pràctics que podreu aplicar en els vostres projectes.
Com funcionen les funcions en JavaScript
Per entendre millor les closures, és important comprendre com funcionen les funcions en JavaScript. En JavaScript, una funció és un objecte que pot ser assignat a una variable, passat com a argument a una altra funció o retornat com a valor d'una funció. Això significa que les funcions poden ser tractades com qualsevol altre tipus de valor.
Quan una funció es defineix en JavaScript, crea un nou entorn d'execució que conté totes les variables i funcions definides dins d'aquesta funció. Aquest entorn s'anomena àmbit lèxic o scope en anglès. L'àmbit lèxic és un objecte intern de la funció que conté totes les variables locals i funcions definides dins d'aquesta.
function outer() {
const outerVariable = "I am accessible from inside";
function inner() {
console.log(outerVariable); // Pot accedir a outerVariable
}
inner();
}
outer(); // Output: "I am accessible from inside"
Quan la funció es crida, es crea un nou objecte d'àmbit d'execució que es col·loca a la part superior de la pila de crides. Aquest objecte conté totes les variables locals i arguments de la funció i està connectat a l'objecte d'àmbit lèxic de la funció que el conté.
Què és una Closure?
Quan una funció acaba d'executar-se, l'objecte d'àmbit d'execució s'elimina de la pila de crides i l'àmbit lèxic es destrueix. En la majoria dels casos, això significa que totes les variables locals i funcions definides dins de la funció desapareixen juntament amb l'àmbit lèxic.
No obstant això, quan una funció crea una closure, l'àmbit lèxic es manté viu fins i tot després que la funció hagi acabat d'executar-se. Això significa que les variables i funcions definides dins de la funció encara són accessibles des de la closure.
Per crear una closure en JavaScript, es defineix una funció dins d'una altra funció i es retorna la funció interna com a resultat de la funció externa:
function counter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const c = counter();
c(); // Output: 1
c(); // Output: 2
c(); // Output: 3
En aquest exemple, la funció counter defineix una variable count i retorna una funció interna que incrementa la variable i la mostra a la consola. La funció interna té accés a l'àmbit lèxic de la funció counter, que conté la variable count.
Quan es crida la funció counter, retorna la funció interna que s'assigna a la variable c. Quan es crida c diverses vegades, cada crida incrementa la variable count i mostra el valor actualitzat. Això és possible perquè la funció interna manté una referència a l'àmbit lèxic de counter, fins i tot després que aquesta hagi finalitzat la seva execució.
Per què són importants les Closures?
Les closures són importants en JavaScript perquè permeten crear funcions més flexibles i potents gràcies a l'accés a variables i funcions definides en el seu entorn de definició. Això ajuda els desenvolupadors a escriure codi més net, modular i reutilitzable.
Encapsulament de variables i funcions
Les closures permeten definir variables i funcions que són privades i no estan disponibles fora de la funció que les defineix. Això ajuda a evitar conflictes de noms i redueix el risc d'errors en el codi.
function createCounter() {
let count = 0; // Variable privada
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
get: function() {
return count;
}
};
}
const myCounter = createCounter();
console.log(myCounter.increment()); // 1
console.log(myCounter.increment()); // 2
console.log(myCounter.decrement()); // 1
console.log(myCounter.get()); // 1
// count no és accessible directament des de fora
Compartir variables entre funcions
Les closures permeten compartir variables i funcions entre diferents funcions, cosa que pot millorar l'eficiència i la llegibilitat del codi.
function createAdder(x) {
return function(y) {
return x + y;
};
}
const add5 = createAdder(5);
const add10 = createAdder(10);
console.log(add5(2)); // 7
console.log(add5(3)); // 8
console.log(add10(2)); // 12
console.log(add10(3)); // 13
En aquest exemple, la funció createAdder retorna una funció que té accés al valor de x definit en l'entorn de definició. Cada crida a createAdder crea una nova closure amb el seu propi valor de x.
Implementació de patrons de disseny
Les closures s'utilitzen habitualment en patrons de disseny com el patró mòdul i el patró fàbrica (Factory Method). El patró mòdul aprofita les closures per crear mòduls amb estat privat i una API pública, mentre que el patró fàbrica utilitza closures per encapsular la lògica de creació d'objectes. Aquests patrons permeten crear codi més modular i reutilitzable, separant la lògica de negoci de la implementació.
const calculatorModule = (function() {
// Variables privades
let history = [];
// Funcions privades
function addToHistory(operation, result) {
history.push({ operation, result, date: new Date() });
}
// API pública
return {
add: function(a, b) {
const result = a + b;
addToHistory(`${a} + ${b}`, result);
return result;
},
subtract: function(a, b) {
const result = a - b;
addToHistory(`${a} - ${b}`, result);
return result;
},
getHistory: function() {
return [...history];
}
};
})();
console.log(calculatorModule.add(5, 3)); // 8
console.log(calculatorModule.subtract(10, 4)); // 6
console.log(calculatorModule.getHistory()); // Array amb l'historial
Casos d'ús pràctics
Gestió d'esdeveniments
Les closures s'utilitzen habitualment per gestionar esdeveniments en JavaScript i permeten mantenir l'estat entre diferents crides:
<button id="btn">Click here</button>
const button = document.getElementById('btn');
function createClickHandler() {
let clickCount = 0;
return function() {
clickCount++;
console.log(`You have clicked ${clickCount} times`);
};
}
const handleClick = createClickHandler();
button.addEventListener('click', handleClick);
En aquest exemple, la funció createClickHandler retorna una funció que gestiona l'esdeveniment de clic al botó. La funció gestora té accés a la variable clickCount, cosa que permet fer un seguiment del nombre de clics.
Funcions de callback
Les closures també s'utilitzen en les funcions de callback per compartir variables entre diferents funcions:
function loadData(url) {
const startTime = Date.now();
fetch(url)
.then(response => response.json())
.then(data => {
const totalTime = Date.now() - startTime;
console.log(`Data loaded in ${totalTime}ms`);
console.log(data);
})
.catch(error => {
const totalTime = Date.now() - startTime;
console.log(`Error after ${totalTime}ms:`, error);
});
}
La funció de callback dins de .then() té accés a startTime gràcies a la closure, la qual cosa permet calcular el temps total de la petició.
Programació asíncrona
Les closures són especialment útils en la programació asíncrona amb async/await:
function createRequestHandler(baseUrl) {
let pendingRequests = 0;
return {
make: async function(endpoint) {
pendingRequests++;
console.log(`Pending requests: ${pendingRequests}`);
try {
const response = await fetch(`${baseUrl}${endpoint}`);
const data = await response.json();
return data;
} finally {
pendingRequests--;
console.log(`Pending requests: ${pendingRequests}`);
}
},
getPending: function() {
return pendingRequests;
}
};
}
const api = createRequestHandler('https://api.example.com');
// Cada crida a api.make() comparteix el mateix comptador de peticions
Errors comuns amb Closures
El problema del bucle amb var
Un dels errors més habituals amb closures és el comportament inesperat dins dels bucles quan s'utilitza var. Vegem-ho amb un exemple:
// ❌ Codi problemàtic
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Output després d'1 segon: 3, 3, 3
Per què passa això? La clau està a entendre que var té àmbit de funció, no de bloc. Això significa que només existeix una única variable i compartida per totes les iteracions del bucle. Quan les funcions del setTimeout s'executen (després d'1 segon), el bucle ja ha finalitzat completament i el valor de i és 3.
Totes les closures creades dins del bucle fan referència a la mateixa variable i, no a una còpia del seu valor en cada iteració.
Solució 1: Utilitzar let
// ✅ Solució amb let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Output després d'1 segon: 0, 1, 2
Amb let, cada iteració del bucle crea una nova variable i amb àmbit de bloc. Cada closure captura la seva pròpia instància de la variable, mantenint el valor correcte.
Solució 2: Crear una closure explícita
Si per algun motiu heu d'utilitzar var, podeu crear una closure que capturi el valor en cada iteració:
// ✅ Solució amb closure explícita (IIFE)
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index);
}, 1000);
})(i);
}
// Output després d'1 segon: 0, 1, 2
Aquí, la funció autoexecutada (IIFE - Immediately Invoked Function Expression) rep i com a paràmetre index. Com que els paràmetres es passen per valor (en el cas de nombres), cada closure captura una còpia independent del valor en aquell moment.
Fuites de memòria (Memory Leaks)
Les closures mantenen referències a les variables del seu àmbit extern. Si no anem amb compte, això pot provocar fuites de memòria:
// ❌ Potencial fuita de memòria
function createHandler() {
const largeData = new Array(1000000).fill('data'); // Array gran
return function() {
// Encara que no utilitzem largeData, la closure manté la referència
console.log('Handler executat');
};
}
const handler = createHandler();
// largeData segueix en memòria mentre handler existeixi
Solució: Assegureu-vos que les closures només capturen les variables que realment necessiten:
// ✅ Millor pràctica
function createHandler() {
const largeData = new Array(1000000).fill('data');
const summary = largeData.length; // Només guardem el que necessitem
return function() {
console.log(`Processat: ${summary} elements`);
};
}
// Ara largeData pot ser recollit pel garbage collector
Resum
Les closures són una eina poderosa en JavaScript que permet:
- Encapsular (encapsulation) variables i funcions de manera privada
- Mantenir l'estat (state persistence) entre diferents crides a una funció
- Crear funcions fàbrica (factory functions) que generen funcions personalitzades
- Implementar patrons de disseny (design patterns) com el patró mòdul (module pattern)
- Gestionar esdeveniments (event handling) i callbacks amb accés a l'àmbit extern (outer scope)
Entendre les closures és fonamental per escriure codi JavaScript més net, modular i eficient. Amb la pràctica, veureu que les closures esdevenen una eina natural en el vostre repertori de programació.