Aller au menu - Aller au contenu

Icône Les closures

Mise à jour : 16/05/2012
Difficulté : Difficile Difficile Creative Commons BY-NC-SA
58 014 visites depuis 7 jours, dont 229 sur ce chapitre classé 6/786
Au cours de la lecture de ce tutoriel, vous avez très probablement dû constater que les fonctions anonymes étaient très fréquemment utilisées pour diverses choses, comme les événements, les isolements de code, etc. Leurs utilisations sont nombreuses et variées, car elles sont très facilement adaptables à toutes les situations. Et s'il y a bien un domaine où les fonctions anonymes excellent, c'est bien les closures !
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Les variables et leurs accès

Avant d'attaquer l'étude des closures, il est de bon ton d'étudier un peu plus en profondeur de quelle manière sont gérées les variables par le Javascript.

Commençons par ce code :

Code : JavaScript
1
2
3
4
5
6
7
function area() {
    var myVar = 1;
}

area(); // On exécute la fonction, ce qui crée la variable « myVar »

alert(myVar);


Même sans l'exécuter, vous vous doutez sûrement du résultat que nous allons obtenir : une erreur. Ceci est normal, car myVar est déclarée dans une fonction tandis que nous essayons d'y accéder depuis l'espace global (en cas d'oubli, nous vous invitons à relire cette sous-partie).

La seule fonction capable d'accéder à myVar est area(), car c'est elle qui l'a créée. Seulement, une fois l'exécution de la fonction terminée, la variable est supprimée et devient donc inaccessible.

Maintenant, si nous faisons ceci :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function area() {

    var myVar = 1;

    function show() {
        alert(myVar);
    }

}

area();

alert(myVar);


Le résultat est toujours le même, il est nul. Cependant, en plus de la fonction area(), la fonction show() est maintenant capable, elle aussi, d'accéder à myVar car elle a été créée dans le même espace que celui de myVar. Mais pour cela il faudrait l'exécuter.

Plutôt que de l'exécuter immédiatement, nous allons l'exécuter une seconde après l'exécution de notre fonction area(), ce qui devrait normalement retourner une erreur puisque myVar est censée être détruite une fois qu'area() a terminé son exécution.

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function area() {

    var myVar = 1;

    function show() {
        alert(myVar);
    }

    setTimeout(show, 1000);

}

area();



Et, par miracle, cela fonctionne ! Vous n'êtes probablement pas surpris, cela fait déjà plusieurs fois que vous savez qu'il est possible d'accéder à une variable même après la disparition de l'espace dans lequel elle a été créée (ici, la fonction area()). Cependant, savez-vous pourquoi ?

Tout ce qui suit est très théorique et ne reflète pas forcément la véritable manière dont les variables sont gérées. Cependant, le principe expliqué vous éclairera tout de même sur ce concept avancé.


Vous souvenez-vous de la formulation « passer une variable par référence » ? Cela signifie que vous permettez que la variable soit accessible par un autre nom que celui d'origine. Ainsi, si vous avez une variable var1 et que vous la passez en référence à var2, alors var1 et var2 pointeront sur la même variable. Donc, en modifiant var1, cela affectera var2, et vice versa.

Tout cela nous amène à la constatation suivante : une variable peut posséder plusieurs références. Dans notre fonction area(), nous avons une première référence vers notre variable, car elle y est déclarée sous le nom myVar. Dans la fonction show(), nous avons une deuxième référence du même nom, myVar.

Quand une fonction termine son exécution, la référence vers la variable est détruite, rendant son accès impossible. C’est ce qui se produit avec notre fonction area(). La variable en elle-même continue à exister tant qu'il reste encore une référence qui est susceptible d'être utilisée. C'est aussi ce qui se produit avec la fonction show(). Puisque celle-ci possède une référence vers notre variable, cette dernière n'est pas détruite.

Ainsi, une variable peut très bien perdre dix de ses références, elle ne sera pas supprimée tant qu'il lui en restera au moins une. C'est ce qui explique que nous puissions accéder à la variable myVar dans la fonction show() malgré la fin de l'exécution de area().

Comprendre le problème

Les closures n'existent pas simplement pour décorer, il existe des raisons bien particulières pour lesquelles elles ont été conçues. Les problèmes qu'elles sont supposées résoudre ne sont pas simples à comprendre, nous allons tâcher de vous expliquer cela au mieux.

Premier exemple



Commençons par un exemple simple qui vous donnera un aperçu de l'ampleur du problème :

Code : JavaScript
1
2
3
4
5
6
7
var number = 1;

setTimeout(function() {
    alert(number);
}, 100);

number++;



Si vous avez essayé le code, alors vous avez sûrement remarqué le problème : la fonction alert() ne nous affiche pas la valeur 1 comme nous pourrions le penser, mais la valeur 2. Nous avons pourtant fait appel à setTimeout() avant le changement de valeur, alors comment se fait-il qu'il y ait ce problème ?

Eh bien, cela vient du fait que ce n'est que la fonction setTimeout() qui a été exécutée avant le changement de valeur. La fonction anonyme, elle, n'est exécutée que 100 millisecondes après l'exécution de setTimeout(), ce qui a largement laissé le temps à la valeur de number de changer.

Si cela vous semble étrange, c’est probablement parce que vous partez du principe que, lorsque nous déclarons notre fonction anonyme, celle-ci va directement récupérer les valeurs des variables utilisées. Que nenni ! Lorsque vous déclarez votre fonction en écrivant le nom d'une variable, vous passez une référence vers cette variable à votre fonction. Cette référence sera ensuite utilisée pour connaître la valeur de la variable, mais seulement une fois la fonction exécutée !

Maintenant que le problème est probablement plus clair dans votre tête, passons à un exemple plus concret !

Un cas concret



Admettons que vous souhaitiez faire apparaître une dizaine de balises <div> de manière progressive, les unes à la suite des autres. Voici le code que vous tenteriez probablement de faire dans l'état actuel de vos connaissances :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var divs = document.getElementsByTagName('div'),
    divsLen = divs.length;

for (var i = 0 ; i < divsLen ; i++) {

    setTimeout(function() {
        divs[i].style.display = 'block';
    }, 200 * i); // Le temps augmentera de 200 ms à chaque élément

}



Alors ? Le résultat n'est pas très concluant, n'est-ce pas ? Si vous jetez un coup d’œil à la console d'erreurs, vous constaterez qu'elle vous signale que la variable divs[i] est indéfinie, et ce dix fois de suite, ce qui correspond à nos dix itérations de boucle. Si nous regardons d'un peu plus près le problème, nous constatons alors que la variable i vaut toujours 10 à chaque fois qu'elle est utilisée dans les fonctions anonymes, ce qui correspond à sa valeur finale une fois que la boucle a terminé son exécution.

Ceci nous ramène au même problème : notre fonction anonyme ne prend en compte que la valeur finale de notre variable. Heureusement, il existe les closures, qui peuvent contourner ce désagrément !

Explorer les solutions

Tout d'abord, qu'est-ce qu'une closure ? En Javascript, il s'agit d'une fonction ayant pour but de capter des données susceptibles de changer au cours du temps, de les enregistrer dans son espace fonctionnel et de les fournir en cas de besoin.

Reprenons notre deuxième exemple et voyons comment lui créer une closure pour la variable i. Voici le code d'origine :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var divs = document.getElementsByTagName('div'),
    divsLen = divs.length;

for (var i = 0 ; i < divsLen ; i++) {

    setTimeout(function() {
        divs[i].style.display = 'block';
    }, 200 * i);

}


Actuellement, le problème se situe dans le fait que la variable i change de valeur avant même que nous n'ayons eu le temps d'agir. Le seul moyen serait donc d'enregistrer cette valeur quelque part. Essayons :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var divs = document.getElementsByTagName('div'),
    divsLen = divs.length;

for (var i = 0 ; i < divsLen ; i++) {

    var currentI = i; // Déclarer une variable DANS une boucle n'est pas conseillé, ici c'est juste pour l'exemple

    setTimeout(function() {
        divs[currentI].style.display = 'block';
    }, 200 * i);

}


Ligne 10, nous utilisons la variable i, car la fonction setTimeout() s'exécute immédiatement, la variable i n'a donc pas le temps de changer de valeur.


Malheureusement, cela ne fonctionne pas, car nous en revenons toujours au même : la variable currentI est réécrite à chaque tour de boucle, car le Javascript ne crée pas d'espace fonctionnel spécifique pour une boucle. Toute variable déclarée au sein d'une boucle est déclarée dans l'espace fonctionnel parent à la boucle. Cela nous empêche donc de converser avec la valeur écrite dans notre variable, car la variable est réécrite à chaque itération de la boucle.

Cependant, il est possible de contourner cette réécriture.

Actuellement, notre variable currentI est déclarée dans l'espace global de notre code. Que se passerait-il si nous la déclarions à l'intérieur d'une IEF ? Eh bien, la variable serait déclarée dans l'espace de la fonction, rendant impossible sa réécriture depuis l'extérieur.

Oui, mais si l'accès à cette variable est impossible depuis l'extérieur, comment peut-on alors l'utiliser pour notre setTimeout() ?


La réponse est simple : en utilisant le setTimeout() dans la fonction contenant la variable ! Essayons :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var divs = document.getElementsByTagName('div'),
    divsLen = divs.length;

for (var i = 0 ; i < divsLen ; i++) {

    (function() {

        var currentI = i;

        setTimeout(function() {
            divs[currentI].style.display = 'block';
        }, 200 * i);

    })();

}



Pratique, non ? Le fonctionnement peut paraître un peu absurde la première fois que l'on découvre ce concept, mais au final il est parfaitement logique.

Étudions le principe actuel de notre code : à chaque tour de boucle, une IEF est créée. À l'intérieur de cette dernière, une variable currentI est déclarée, puis nous lançons l'exécution différée d'une fonction anonyme faisant appel à cette même variable. Cette dernière fonction va utiliser la première (et la seule) variable currentI qu'elle connaît, celle déclarée dans notre IEF, car elle n'a pas accès aux autres variables currentI déclarées dans d'autres IEF.

Vous n'avez toujours pas oublié la première sous-partie de ce chapitre, n'est-ce pas ? Car, si nous avons traité le sujet des variables, c'est pour vous éviter une mauvaise compréhension à ce stade du chapitre. Ici nous avons un cas parfait de ce que nous avons étudié : currentI est déclarée dans une IEF, sa référence est donc détruite à la fin de l'exécution de l'IEF. Cependant, nous y avons toujours accès dans notre fonction anonyme exécutée en différé, car nous possédons une référence vers cette variable, ce qui évite sa suppression.

Dernière chose, vous risquerez de tomber assez fréquemment sur des closures plutôt écrites de cette manière :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var divs = document.getElementsByTagName('div'),
    divsLen = divs.length;

for (var i = 0 ; i < divsLen ; i++) {

    (function(currentI) {

        setTimeout(function() {
            divs[currentI].style.display = 'block';
        }, 200 * i);

    })(i);

}


Concrètement, qu'est-ce que l'on a fait ? Eh bien, nous avons tout simplement créé un argument currentI pour notre IEF et nous lui passons en paramètre la valeur de i. Cette modification fait gagner un peu d'espace (suppression de la ligne 8) et permet de mieux organiser le code, on distingue plus facilement ce qui constitue la closure ou non.

Tant que nous y sommes, nous pouvons nous permettre d'apporter une modification de plus à la ligne 6 :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var divs = document.getElementsByTagName('div'),
    divsLen = divs.length;

for (var i = 0 ; i < divsLen ; i++) {

    (function(i) {

        setTimeout(function() {
            divs[i].style.display = 'block';
        }, 200 * i);

    })(i);

}



Ainsi, même dans la closure, nous utilisons une variable nommée i. Cela est bien plus pratique à gérer et prête moins à confusion pour peu que l'on ait compris que dans la closure nous utilisons une variable i différente de celle située en-dehors de la closure.

Voilà, vous savez maintenant vous servir des closures dans leur cadre général. Bien qu'elles existent sous plusieurs formes et pour plusieurs cas d'utilisation, nous avons ici étudié le cas principal.

Une autre utilité, les variables statiques

Nous venons de voir un cas d'utilisation des closures. Cependant, leur utilisation ne se limite pas uniquement à ce cas de figure, elles permettent de résoudre de nombreux casse-têtes en Javascript. Un cas provoquant assez souvent quelques prises de tête dans ce langage est l’inexistence d'un système natif de variables statiques.

Si vous avez déjà codé avec quelques autres langages, vous avez probablement déjà étudié les variables statiques. En C, elles se présentent sous cette forme :

Code : C
1
2
3
void myFunction() {
    static int myStatic = 0;
}


Ces variables particulières sont déclarées à la première exécution de la fonction, mais ne sont pas supprimées à la fin des exécutions. Elles sont conservées pour les prochaines utilisations de la fonction.

Ainsi, dans ce code en C, la variable myStatic est déclarée et initialisée à 0 lors de la première exécution de myFunction(). La prochaine exécution de la fonction ne déclarera pas de nouveau cette variable, mais la réutilisera avec la dernière valeur qui lui a été affectée.

En gros, c'est comme si vous déclariez une variable globale en Javascript et que vous l'utilisiez dans votre fonction : la variable et sa valeur ne seront jamais détruites. En revanche, la variable globale est accessible par toutes les fonctions, tandis qu'une variable statique n'est accessible que pour la fonction qui a fait sa déclaration.

En Javascript, nous pouvons faire ceci :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var myVar = 0;

function display(value) {

    if(typeof value != 'undefined') {
        myVar = value;
    }

    alert(myVar);

}

display();   // Affiche : 0
display(42); // Affiche : 42
display();   // Affiche : 42


Alors que nous voudrions arriver à ceci afin d'éviter l'accès à myVar par une fonction autre que display() :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function display(value) {

    static var myVar = 0;

    if(typeof value != 'undefined') {
        myVar = value;
    }

    alert(myVar);

}

display();   // Affiche : 0
display(42); // Affiche : 42
display();   // Affiche : 42


Je viens de voir que le mot-clé static existe en Javascript, pourquoi ne pas l'utiliser ?


Ah oui ! Il s'agit d'une petite incohérence (de plus) en Javascript. Il faut savoir que ce langage a réservé de nombreux mots-clés alors qu'ils lui sont inutiles. Le mot-clé static en fait partie. Autrement dit, il est réservé, mais ne sert à rien et n'a donc aucune influence sur votre code (mis à part le fait de générer une erreur).

La solution se trouve donc avec les closures. En respectant le schéma classique d'une closure, une IEF avec une fonction anonyme à l'intérieur, nous pouvons déclarer une variable dans l'IEF et ainsi elle ne sera utilisable que par la fonction anonyme et ne sera jamais supprimée :

Code : JavaScript
1
2
3
4
5
6
7
8
9
(function() {

    var myVar = 0;

    function() {
        // Du code…
    }

})();


Cependant, comment accéder à notre fonction anonyme ? La solution est simple : en la retournant avec le mot-clé return et en passant sa référence à une variable :

Code : JavaScript
1
2
3
4
5
6
7
8
9
var myFunction = (function() {

    var myVar = 0;

    return function() {
        // Du code…
    };

})();


Si nous reprenons notre exemple, mais adapté de manière à ce qu'il possède une variable statique, alors nous obtenons ceci :

Code : JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var display = (function() {

    var myVar = 0; // Déclaration de la variable pseudo-statique

    return function(value) {

        if(typeof value != 'undefined') {
            myVar = value;
        }

        alert(myVar);

    };

})();

display();   // Affiche : 0
display(42); // Affiche : 42
display();   // Affiche : 42



Et voilà une fonction avec une variable statique nommée myVar ! Cela pourra vous être utile par moments (bien que cela soit assez rare).

En résumé


  • Une variable peut posséder plusieurs références. Elle ne sera jamais supprimée tant qu'elle possèdera encore une référence active.
  • Les closures ont été inventées dans le but de répondre à plusieurs problématiques concernant la gestion de données.
  • Une closure peut être écrite de plusieurs manières différentes, à vous de choisir celle qui convient le mieux à votre code.
Chapitre précédent Sommaire Chapitre suivant

Partager

8 commentaires pour "Les closures"
Note moyenne : 3.67 / 4 (873 votes)
Pseudo Commentaire
Hors ligne Nesquik69 # Posté le 05/03/2012 à 16:24:32
Slave of the Rythm
Avatar
Groupe : Auteurs

Ville : Tassin la demi-lune
Pays : France métropolitaine

Tant mieux car j'avais peur que cela soit très compliqué pour les lecteurs. C'est un sujet assez technique et le concept n'est pas simple à comprendre. En plus de cela, je n'ai eu aucun commentaire depuis son écriture, hormis le tiens maintenant ^^ .

Mes sites web : Plune | It's Friday!
Mes tutos : Javascript | Ajax
Mes projets : SpeedTest | Boxifier | zUploader
Articles : Breakpoint 2007 / 2008 / 2009
Useless : Mangas | Caramelldansen | Monde parallèle acidifié | Tydax
 
Hors ligne Nicolas M. # Posté le 06/03/2012 à 16:29:52
M(NiCoLaSm) = 406,9 g/mol
Avatar

Avis : Très bon

Ville : Notre-dame de bondeville
Pays : France métropolitaine

J’ai lu et relu l’autre tutoriel sur le sujet, sans jamais comprendre ce que c’était que cette histoire de fonction qui retournait une fonction au setTimeout… Le tutoriel présentait surtout l’intérêt pour les setTimeout dans une boucle…

Alors que votre tutoriel explique beaucoup mieux pourquoi on a une « IEF » dans la boucle, et la distingue bien plus nettement de la fonction qu’on envoie au setTimeout … :)

Image utilisateur Image utilisateur

Le saviez-vous ? Les forums sont environ 283 174 fois plus efficaces que ma boîte MP pour vous aider. ;)
Image utilisateur
 
Hors ligne Kineolyan # Posté le 17/03/2012 à 13:02:24

Avis : Très bon

Études : Ecole Centrale de Paris

Merci pour le tutoriel.
Effectivement, le concept de closures n'est pas évident, mais je trouve que les exemples sont très intéressants.
Donc merci pour ces explications, ça peut m'être utile.

-Kineolyan
 
Hors ligne LCaba # Posté le 13/05/2012 à 19:22:33
A dollar is what I need
Avatar

Avis : Très bon

Ville : Toulouse
Pays : France métropolitaine

Salut, je suis tombé sur ce site : http://www.jslint.com/

Pour mes IEF, j'avait l'erreur suivante :

Citation : JSLint
Move the invocation into the parens that contain the function.


En regardant le code d'implémentation de JSON par Douglas Crockford, j'ai découvert qu'il est préférable d'écrire une IEF comme suit :

Code : JavaScript
1
2
3
(function(){
	// closure
}());

Codez couverts grâce à "use strict" !
MUSIQUE : découvrez mon frère, Kryl
Image utilisateur
 
Hors ligne Nesquik69 # Posté le 13/05/2012 à 23:57:14
Slave of the Rythm
Avatar
Groupe : Auteurs

Ville : Tassin la demi-lune
Pays : France métropolitaine

Je connais cette manière de faire mais, honnêtement, c'est vraiment du chipotage, ça ne change quasiment pas grand chose ;) .

Mes sites web : Plune | It's Friday!
Mes tutos : Javascript | Ajax
Mes projets : SpeedTest | Boxifier | zUploader
Articles : Breakpoint 2007 / 2008 / 2009
Useless : Mangas | Caramelldansen | Monde parallèle acidifié | Tydax
 

Voir tous les commentaires