Programmation JavaScript/Fermeture

Une fermeture ou clôture (appelée closure en anglais) est une fonction accompagnée de son environnement lexical. L'environnement lexical d'une fonction est l'ensemble des variables non locales qu'elle a capturées, soit par valeur (c'est-à-dire par copie des valeurs des variables), soit par référence (c'est-à-dire par copie des adresses mémoires des variables). Une fermeture est donc créée, entre autres, lorsqu'une fonction est définie dans le corps d'une autre fonction et utilise des paramètres ou des variables locales de cette dernière.

Une fermeture peut être passée en argument d'une fonction dans l'environnement où elle a été créée (passée vers le bas) ou renvoyée comme valeur de retour (passée vers le haut). Dans ce cas, le problème posé alors par la fermeture est qu'elle fait référence à des données qui auraient typiquement été allouées sur la pile d'exécution et libérées à la sortie de l'environnement. Hors optimisations par le compilateur, le problème est généralement résolu par une allocation sur le tas de l'environnement.

Itérations

modifier

En javascript, il est possible de créer explicitement des fermetures pour conserver des valeurs dans un contexte particulier. Par exemple, dans une fonction qui crée des boutons dans une boucle en définissant une fonction d'écoute du clic de l'utilisateur a besoin d'utiliser une fermeture.

La fonction de l'exemple ci-dessous crée des boutons pour ouvrir des panneaux cachés initialement, sans utiliser de fermeture :

function creerBoutons()
{
	var boutons = document.querySelectorAll(".panel-bouton-ouvrir");
	var panels = document.querySelectorAll(".panel-ouvrable");
	for(var i=0 ; i<boutons.length ; i++)
	{
		var bouton = boutons[i];  // Le bouton déclenchant l'ouverture/fermeture
		var panel = panels[i];    // Le panel caché initialement
		bouton.addEventListener('click', function(){togglePanel(bouton, panel)});
	}
}

Le problème est visible lorsque la page contient plusieurs boutons et plusieurs panels : tous les boutons n'activent que le dernier panel et seul le dernier bouton change d'état même si c'est un autre bouton qui est cliqué.

La cause est la fonctionnalité hoisting de Javascript : les déclarations de variables avec var sont remontées au niveau de la fonction. Le code précédent est interprété comme ceci :

function creerBoutons()
{
	var boutons = document.querySelectorAll(".panel-bouton-ouvrir");
	var panels = document.querySelectorAll(".panel-ouvrable");
    var bouton, panels;
	for(var i=0 ; i<boutons.length ; i++)
	{
		bouton = boutons[i];  // Le bouton déclenchant l'ouverture/fermeture
		panel = panels[i];    // Le panel caché initialement
		bouton.addEventListener('click', function(){togglePanel(bouton, panel)});
	}
}

Il est facile de comprendre la cause du problème : lors de l'appel à la fonction, les variables bouton et panel sont les mêmes pour toutes les fonctions d'écoute et contiennent les valeurs qui leurs ont été assignées lors de la dernière itération. Seuls le dernier bouton et le dernier panel sont donc fonctionnels.

La solution est de forcer la création de nouvelles variables à chaque itération pour conserver les valeurs assignées, utilisées par les fonctions d'écoute des différents boutons. Pour cela, une fermeture est nécessaire :

function creerBoutons()
{
	var boutons = document.querySelectorAll(".panel-bouton-ouvrir");
	var panels = document.querySelectorAll(".panel-ouvrable");
	for(var i=0 ; i<boutons.length ; i++)
	(function () {
		var bouton = boutons[i];  // Le bouton déclenchant l'ouverture/fermeture
		var panel = panels[i];    // Le panel caché initialement
		bouton.addEventListener('click', function(){togglePanel(bouton, panel)});
	})();
}

Le bloc d'instruction est encapsulé dans une fonction anonyme, appelée immédiatement. Les variables qui y sont déclarées sont déjà au niveau le plus haut de la fonction. Les valeurs qui leurs sont assignées proviennent des variables locales de la fonction englobante et sont les copies des valeurs au moment de l'appel à la fonction anonyme durant l'itération de la boucle for.

En JavaScript 6 (EcmaScript 6), il est possible d'utiliser let ou const à la place de var pour déclarer les variables, car le hoisting n'est pas opérant avec ces mots-clés.

Créer des fonctions de manière dynamique

modifier

Les fermetures peuvent être utilisées aussi pour créer des fonctions de manière dynamique, comme l'exemple ci-dessous créant des fonctions multiplicatrice par un facteur passé en argument.

function creerMultiplicationPar(facteur)
{
	function multiplier(nombre)
	{
		// Accède à son argument et celui de la fonction qui l'englobe
		return nombre * facteur;
	}
	return multiplier; // Retourne la fonction locale
}

var doubler = creerMultiplicationPar(2);
var tripler = creerMultiplicationPar(3);

console.log(doubler(100)); // -> 200
console.log(tripler(100)); // -> 300

Comme une fonction est utilisable comme n'importe quel type de données en JavaScript, on peut écrire :

function creerMultiplicationPar(facteur)
{
	return function(nombre)
	{
		// Accède à son argument et celui de la fonction qui l'englobe
		return nombre * facteur;
	}; // Retourne la fonction locale
}

En ES6 :

function creerMultiplicationPar(facteur)
{
	return (nombre) => nombre * facteur;
}