Mis à jour le mercredi 5 juillet 2017
  • 40 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Ce cours existe en eBook.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les closures

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

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 !

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 :

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 :

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.

function area() {

    var myVar = 1;

    function show() {
        alert(myVar);
    }

    setTimeout(show, 1000);

}

area();

Essayer le code

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 ?

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 :

var number = 1;

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

number++;

Essayer le code

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 :

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

}

Essayer le code

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 :

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 :

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);

}

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 IIFE ? 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 :

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);

    })();

}

Essayer le code

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 IIFE 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 IIFE, car elle n'a pas accès aux autres variables currentI déclarées dans d'autres IIFE.

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 IIFE, sa référence est donc détruite à la fin de l'exécution de l'IIFE. 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 :

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 IIFE 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 :

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);

}

Essayer le code final

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 :

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 :

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() :

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 IIFE avec une fonction anonyme à l'intérieur, nous pouvons déclarer une variable dans l'IIFE et ainsi elle ne sera utilisable que par la fonction anonyme et ne sera jamais supprimée :

(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 :

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 :

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

Essayer le code

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.

Exemple de certificat de réussite
Exemple de certificat de réussite