Informatique


Les closures

Aller à


Wablief closures?

wablief?

Une closure c'est construire un environnement (un contexte, un scope) au sein duquel une variable locale persiste tant que le code en a besoin.

Donc, même après l'exécution de la fonction au sein de laquelle la variable a été déclarée, cette variable persiste au sein d'un environnement particulier.
La compréhension des closures est indispensables pour les développeurs JavaScript et donc indispensable à ceux qui désirent utiliser les frameworks.


Un premier exemple

On va créer un compteur, à chaque appel de la fonction le compteur sera incrémenté de 1.

Premier approche: avec une globale
Démo
<script>
var cpt=0;
function compteur(){
	cpt=cpt+1;
	return cpt;
}
document.write(compteur());
document.write(compteur());
document.write(compteur());
document.write(compteur());
</script>

Çà fonctionne mais on traîne une globale tout au long du script, cette solution doit être évitée.

Deuxième approche: avec une locale
Démo
<script>
function compteur(){
	var cpt=0;
	cpt=cpt+1;
	return cpt;
}
document.write(compteur());
document.write(compteur());
document.write(compteur());
document.write(compteur());
</script>

Le compteur renvoie systématiquement 1 !

Pourquoi? La variable cpt est créée et initialisée à l'entrée de la fonction compteur, elle sera donc recréée et affectée à chaque exécution de la fonction compteur().

Troisième approche: avec une fonction imbriquée
Démo
<script>
function compteur(){
	var cpt=0;
		function ajouter(){
			cpt=cpt+1;
		}
	ajouter();
	return cpt;
}
document.write(compteur());
document.write(compteur());
document.write(compteur());
document.write(compteur());
</script>

Le compteur renvoie systématiquement 1 !

Pourquoi? La variable cpt est créée et initialisée à l'entrée de la fonction compteur, elle sera donc recréée et affectée à chaque exécution de la fonction compteur().

Quatrième approche: on extériorise la fonction ajouter()
Démo
<script>
"use strict";
function ajouter(cpt){
	cpt=cpt+1;
	return cpt;
	}

function compteur(){
	var cpt=0;
	cpt=ajouter(cpt);
	return cpt;
}
document.write(compteur());
document.write(compteur());
document.write(compteur());
document.write(compteur());

</script>

Rien y fait, le compteur renvoie systématiquement 1 !

Pourquoi? La variable cpt est créée et initialisée à l'entrée de la fonction compteur, elle sera donc recréée et affectée à chaque exécution de la fonction compteur().


Solution: la closure

On affecte une fonction anonyme à une variable, cette variable-fonction retournera une fonction fille, celle-ci pourra bénéficier du scope de sa parente!

Démo
<script>
var compteur=function(){
	cpt=0;
	return function ajouter(){
		cpt=cpt+1;return cpt;
		}
};
var go=compteur();
document.write(go());
document.write(go());
document.write(go());
document.write(go());
</script>

Et la version plus condensée, en auto-exécutant la fonction anonyme.

Démo
<script>
compteur=(function(){
	cpt=0;
	return function ajouter(){
		cpt=cpt+1;
		return cpt;
		}
})();
document.write(compteur());
document.write(compteur());
document.write(compteur());
document.write(compteur());
</script>

C'est bon, çà fonctionne.

Pourquoi?

  • Dans le premier document.write()la fonction est auto-exécutée et renvoie une expression: function ajouter(){cpt=cpt+1;return cpt;}
  • Dans le deuxième document.write()la définition de la fonction retournée est exécutée (à cause des parenthèses), elle renvoie 1
  • La variable cpt a été déclarée dans le scope parent (la fonction anonyme), étant donné que cpt doit être utilisé dans une fonction fille, la variable est maintenue dans la mémoire! Une variable du scope parent qui ne serait pas utlisée dans une fonction fille serait supprimée par l'interpréteur!
  • le contenu de la variable cpt est donc sauvegardé, le scope de la fonction compteur est maintenu pour toutes les variables utilisées dans les fonctions filles!!!
  • et donc, à chaque appel de compteur() on retrouve cpt incrémenté comme attendu.

Une closure représente une fonction fille qui, définie à l'intérieur d'une fonction parente, a accès aux variables déclarées dans la fonction parente même après l'exécution de la fonction parente.
En résumé:

Finalement, on pourrait dire la même chose du script principal ! Si on considère que le script en lui-même est une fonction, et c'est le cas, alors les fonctions filles du script principal ont toujours accès aux variables définies au niveau du script tout au long de l'exécution de ce script.

Bref, créer une closure c'est en quelque sorte créer un contexte ou un environnement particulier au sein duquel la fonction parente peut-être considérée comme un script complet.

Normalement une variable locale à une fonction est détruite à l'issue de l'exécution de cette fonction, mais, si une fonction est définie à l'intérieur d'une fonction les variables de la fonction parente reste en mémoire afin de permettre à la fonction fille de pouvoir s'exécuter:

  • il y a maintien d'un contexte d'exécution pour la fonction fille
  • ce contexte est restitué à chaque appel de la fonction fille
  • ce contexte peut évidemment évoluer tout au long du programme.

Un deuxième exemple

On propose de récupérer, toutes les 2 secondes, la valeur d'aun compteur de boucle.

On déclare le compteur de boucle avec var
Démo
	for(var i=0;i<10;i++){
		setTimeout(function(){document.write(i)},i*2000);
	}

JS est asynchrone, c'est l'interpréteur qui ordonne les instructions à effectuer afin que l'exécution des instructions soit la plus rapide: il incrémente d'abord le compteur i et exécute ensuite 10 fois l'affichage (après 2sec) de chaque fois la valeur 10!

On déclare le compteur de boucle avec let
Démo
	for(let i=0;i<10;i++){
		setTimeout(function(){document.write(i)},i*2000);
	}

le compteur i étant détruit après la boucle, l'interpréteur a prévu de retenir toutes les valeurs de i!
let nous a permis d'éviter une closure!

Solution avec une closure
Démo
function boucle(i){
for(let i=0;i<10;i++){
	affic(i);
	}
}
function affic(i){
	setTimeout(function(){
		document.write(i)
		},i*2000);
}
boucle();