Aller au contenu principal

Closures

Soumis par lulu le lun 12/06/2023 - 11:12

Variables globales et locales

Les variables JavaScript peuvent avoir une portée locale ou globale.

Une fonction peut accéder aux variables définies à l'intérieur de la fonction (portée locale) :

function myFunction() { 
  let a = 4; 
  document.getElementById("demo").innerHTML = a * a; 


// Affiche "16" dans l'élément "demo" 
myFunction();

Mais une fonction peut également accéder à des variables définies en dehors de la fonction, comme ceci (portée globale) :

let a = 4; 
function myFunction() { 
  document.getElementById("demo").innerHTML = a * a;  
}  

// Affiche également "16" dans l'élément "demo"  
myFunction();

  • Dans le dernier exemple, a est une variable globale.
  • Dans une page Web, les variables globales appartiennent à la page.
  • Les variables globales peuvent être utilisées (et modifiées) par tous les autres scripts de la page.
  • Dans le premier exemple, a est une variable locale.
  • Une variable locale ne peut être utilisée qu'à l'intérieur de la fonction dans laquelle elle est définie. Elle est cachée des autres fonctions et des autres codes de script.
  • Les variables globales et locales portant le même nom sont des variables différentes. La modification de l'une ne modifie pas l'autre.
  • Les variables créées sans mot-clé de déclaration (var, let ou const) sont toujours globales, même si elles sont créées dans une fonction :

function myFunction() {  
  a = 4;  
}  

// exécution de la fonction  
myFunction();  

// a est considérée comme une variable globale, bien que déclarée dans une fonction,  
// et la valeur "16" sera donc affichée dans l'élément "demo"  
document.getElementById("demo").innerHTML = a * a;

Durée de vie des variables

Les variables globales vivent jusqu'à ce que la page soit supprimée, par exemple lorsque vous naviguez vers une autre page ou fermez la fenêtre.

Les variables locales ont une durée de vie courte. Elles sont créées lorsque la fonction est invoquée, et supprimées lorsque la fonction est terminée.

Le dilemme du compteur

Supposons que vous souhaitiez utiliser une variable pour compter quelque chose et que vous vouliez que ce compteur soit disponible pour toutes les fonctions.

Vous pourriez utiliser une variable globale et une fonction pour augmenter le compteur :

// Initialiser le compteur  
let counter = 0 ;  

// Fonction pour incrémenter le compteur  
fonction add() {  
    counter += 1 ;  
}  

// Appelle add() 3 fois  
add() ;  
add() ;  
add() ;  

// Le compteur est maintenant à 3  
document.getElementById("demo").innerHTML = "The counter is: " + counter;

 Il y a toutefois un problème avec la solution ci-dessus : tout code sur la page peut modifier le compteur, sans appeler add().

Le compteur devrait être local à la fonction add(), pour éviter qu'un autre code ne le modifie :

// Initialiser le compteur  
let counter = 0 ;  

// Fonction pour incrémenter le compteur  
fonction add() {  
    let counter = 0 ;  
    counter += 1 ;  
}  

// Appelle add() 3 fois  
add() ;  
add() ;  
add() ;  

// Le compteur devrait maintenant être à 3. Mais il est à 0 !  
// Le résultat n'est pas 3 car vous mélangez le compteur global et le compteur local.  
document.getElementById("demo").innerHTML = "The counter is: " + counter;

 Cela n'a pas fonctionné car nous avons affiché le compteur global au lieu du compteur local.

Nous pouvons supprimer le compteur global et accéder au compteur local en laissant la fonction le retourner :

// Fonction d'incrémentation du compteur  
function add() {  
    let counter = 0 ;  
    counter += 1 ;  
    return counter ;  
}  

// Appelle add() 3 fois  
add() ;  
add() ;  
add() ;  

// Le compteur devrait maintenant être à 3. Mais il est à 1.

Cela n'a pas fonctionné car nous remettons à zéro le compteur local à chaque fois que nous appelons la fonction.

 Une fonction interne JavaScript peut résoudre ce problème.

Fonctions imbriquées JavaScript

Toutes les fonctions ont accès à la portée globale.  

En fait, en JavaScript, toutes les fonctions ont accès à la portée "au-dessus" d'elles.

JavaScript prend en charge les fonctions imbriquées (nested functions). Les fonctions imbriquées ont accès à la portée "au-dessus" d'elles.

Dans l'exemple ci-dessous, la fonction interne plus() a accès à la variable counter de la fonction parent :

function add() {  
    let counter = 0;  
    function plus() {counter += 1;}  
    plus();  
    return counter;  
}

Cela aurait pu résoudre le dilemme du compteur, si nous pouvions atteindre la fonction plus() depuis l'extérieur.

Nous devrions également trouver un moyen de n'exécuter counter = 0 qu'une seule fois.

Nous avons besoin d'une closure.

Closures JavaScript

const add = (function () { 
    let counter = 0 ;  
    return function () {  
        counter += 1 ;  
        return counter  
    }  
})() ;  

add() ;  
add() ;  
add() ;  

// le compteur est maintenant à 3

  • La variable add est affectée à la valeur de retour d'une fonction auto-invoquante (self-invoking function).
  • La fonction auto-invoquante ne s'exécute qu'une seule fois. Elle met le compteur à zéro (0) et renvoie une expression de fonction (function expression).
  • De cette façon, add devient une fonction. La partie "merveilleuse" est qu'elle peut accéder au compteur dans la portée parent.
  • C'est ce qu'on appelle une closure (fermeture) JavaScript. Elle permet à une fonction d'avoir des variables "privées".
  • Le compteur est protégé par la portée de la fonction anonyme et ne peut être modifié que par la fonction add.
  • Une closure est une fonction qui a accès à la portée du parent, même après la fermeture de la fonction parent. 

Une closure est une fonction interne qui va « se souvenir » et pouvoir continuer à accéder à des variables définies dans sa fonction parente même après la fin de l’exécution de celle-ci.

function compteur() {  
    let count = 0;  

    return function() {  
        return count++;  
    };  
}  

let plusUn = compteur();

Comme vous le voyez, on crée une fonction compteur(). Cette fonction initialise une variable count et définit également une fonction anonyme interne qu’elle va retourner. Cette fonction anonyme va elle-même tenter d’incrémenter (ajouter 1) la valeur de let count définie dans sa fonction parente.

Ici, si on appelle notre fonction compteur() directement, le code de notre fonction anonyme est renvoyé mais n’est pas exécuté puisque la fonction compteur() renvoie simplement une définition de sa fonction interne.

Pour exécuter notre fonction anonyme, la façon la plus simple est donc ici de stocker le résultat retourné par compteur() (notre fonction anonyme donc) dans une variable et d’utiliser ensuite cette variable « comme » une fonction en l’appelant avec un couple de parenthèses. On appelle cette variable let plusUn.

A priori, on devrait avoir un problème ici puisque lorsqu’on appelle notre fonction interne via notre variable plusUn, la fonction compteur() a déjà terminé son exécution et donc la variable count ne devrait plus exister ni être accessible.

Pourtant, si on tente d’exécuter code, on se rend compte que tout fonctionne bien :

function compteur() {  
    let count = 0;  

    return function() {  
        return count++;  
    };  
}  

let plusUn = compteur();  

alert(plusUn()); // vaut 0  
alert(plusUn()); // vaut 1  
alert(plusUn()); // vaut 2

C’est là tout l’intérêt et la magie des closures : si une fonction interne parvient à exister plus longtemps que la fonction parente dans laquelle elle a été définie, alors les variables de cette fonction parente vont continuer d’exister au travers de la fonction interne qui sert de référence à celles-ci.

Lorsqu’une fonction interne est disponible en dehors d’une fonction parente, on parle alors de closure ou de « fermeture » en français.

Le code ci-dessus présente deux intérêts majeurs : tout d’abord, notre variable count est protégée de l’extérieur et ne peut être modifiée qu’à partir de notre fonction anonyme. Ensuite, on va pouvoir réutiliser notre fonction compteur() pour créer autant de compteurs qu’on le souhaite et qui vont agir indépendamment les uns des autres. Regardez plutôt l’exemple suivant pour vous en convaincre :

function compteur() {  
    let count = 0;  

    return function() {  
        return count++;  
    };  
}  

let plusUn = compteur();  
let plusUnBis = compteur();  

alert(plusUn()); // vaut 0  
alert(plusUn()); // vaut 1  
alert(plusUnBis()); // vaut 0  
alert(plusUn()); // vaut 2  
alert(plusUnBis()); // vaut 1

Présentation des closures dans le livre "JavaScript pour le web 2.0" (Arnaud Gougeon - Thierry Templier)

Une closure est une fonction JavaScript particulière, qui utilise directement des variables définies en dehors de la portée de son code. Ce mécanisme est souvent utilisé par des fonctions définies dans d'autres fonctions, comme l'illustre le code suivant :

function uneFonction(parametre) { 
    function uneClosure(unAutreParametre) { 
        return "Les paramètres sont : " 
            + parametre + ", " + unAutreParametre; 
    } 
    return uneClosure; 
}

var retour = uneFonction("Mon paramètre"); 
var valeurRetour = retour("Mon autre paramètre"); 
// valeurRetour contient "Les paramètres sont : Mon paramètre, Mon autre paramètre"

Cette fonctionnalité très puissante de JavaScript est couramment utilisée pour mettre en œuvre les concepts de la programmation objet avec ce langage.

Il convient cependant d'utiliser prudemment ce concept puisque certains navigateurs, notamment Internet Explorer, ne gèrent pas les closures correctement dans le cas de références cycliques, entraînant des fuites de mémoire.

Qu’est-ce qu’une fonction anonyme et quels sont les cas d’usage ?

Les fonctions anonymes sont, comme leur nom l’indique, des fonctions qui ne vont pas posséder de nom. En effet, lorsqu’on crée une fonction, nous ne sommes pas obligés de lui donner un nom à proprement parler.

Généralement, on utilisera les fonctions anonymes lorsqu’on n’a pas besoin d’appeler notre fonction par son nom c’est-à-dire lorsque le code de notre fonction n’est appelé qu’à un endroit dans notre script et n’est pas réutilisé.

En d’autres termes, les fonctions anonymes vont très souvent simplement nous permettre de gagner un peu de temps dans l’écriture de notre code et (bien que cela porte à débat) de le rendre plus clair en ne le polluant pas avec des noms inutiles.

On va pouvoir créer une fonction anonyme de la même façon qu’une fonction classique, en utilisant le mot clef function mais en omettant le nom de la fonction après.

Regardez plutôt le code ci-dessous :

function(){  
    alert('Alerte exécutée par une fonction anonyme');  
}

Nous avons ici déclaré une fonction anonyme donc le rôle est d’exécuter une fonction alert() qui va elle-même renvoyer le message « Alerte exécutée par une fonction anonyme » dans une boite d’alerte.

A ce niveau, pourtant, nous faisons face à un problème : comment appeler une fonction qui n’a pas de nom ?

On va avoir plusieurs façons de faire en JavaScript. Pour exécuter une fonction anonyme, on va notamment pouvoir :

  • Enfermer le code de notre fonction dans une variable et utiliser la variable comme une fonction ;
  • Auto-invoquer notre fonction anonyme ;
  • Utiliser un évènement pour déclencher l’exécution de notre fonction.

Exécuter une fonction anonyme en utilisant une variable

Voyons ces différentes façons de faire en détail, en commençant par la plus simple : enfermer la fonction dans une variable et utiliser la variable comme une fonction.

let alerte = function(){  
    alert('Alerte exécutée par une fonction anonyme');  
}  

alerte();

Ici, on affecte notre fonction anonyme à une variable nommée let alerte. Notre variable contient donc ici une valeur complexe qui est une fonction et on va désormais pouvoir l’utiliser comme si c’était une fonction elle-même.

Pour « appeler notre variable » et pour exécuter le code de la fonction anonyme qu’elle contient, il va falloir écrire le nom de la variable suivi d’un couple de parenthèses. Ces parenthèses sont des parenthèses dites « appelantes » car elles servent à exécuter la fonction qui les précède.

Auto-invoquer une fonction anonyme

La deuxième façon d’exécuter une fonction anonyme va être de créer une fonction anonyme qui va s’auto-invoquer c’est-à-dire qui va s’invoquer (ou s’appeler ou encore s’exécuter) elle-même dès sa création.

Pour créer une fonction auto-invoquée à partir d’une fonction, il va tout simplement falloir rajouter un couple de parenthèses autour de la fonction et un second après le code de la fonction.

Nous avons vu précédemment que le couple de parenthèses suivant le nom de notre variable stockant notre fonction anonyme servait à lancer l’exécution de la fonction.

De la même manière, le couple de parenthèses après la fonction va faire en sorte que la fonction s’appelle elle-même.

// Fonction anonyme auto-invoquée  
(function(){alert('Alerte exécutée par une fonction anonyme')})();  

// Fonction nommée auto-invoquée  
(function bonjour(){alert('Bonjour !')})();

Vous pouvez noter deux choses à propos des fonction auto-invoquées. Tout d’abord, vous devez savoir que la notion d’auto-invocation n’est pas réservée qu’aux fonctions anonymes : on va tout à fait pouvoir auto-invoquer une fonction qui possède un nom. Cependant, en pratique, cela n’aura souvent pas beaucoup d’intérêt (puisque si une fonction possède un nom, on peut tout simplement l’appeler en utilisant ce nom).

Ensuite, vous devez bien comprendre que lorsqu’on auto-invoque une fonction, la fonction s’exécute immédiatement et on n’a donc pas de flexibilité par rapport à cela : une fonction auto-invoquée s’exécutera toujours juste après sa déclaration. 

Exécuter une fonction anonyme lors du déclenchement d’un évènement

On va enfin également pouvoir rattacher nos fonctions anonymes à ce qu’on appelle des « gestionnaires d’évènements » en JavaScript.

Le langage JavaScript va en effet nous permettre de répondre à des évènements, c’est-à-dire d’exécuter certains codes lorsqu’un évènement survient.

Le JavaScript permet de répondre à de nombreux types d’évènements : clic sur un élément, pressage d’une touche sur un clavier, ouverture d’une fenêtre, etc.

Pour indiquer comment on veut répondre à tel évènement, on utilise des gestionnaires d’évènements qui sont des fonctions qui vont exécuter tel code lorsque tel évènement survient.

On va pouvoir passer une fonction anonyme à un gestionnaire d‘évènement qui va l’exécuter dès le déclenchement de l’évènement que le gestionnaire prend en charge.

Pour un exemple concret du fonctionnement général de la prise en charge d’évènements et de l’utilisation des fonctions anonymes, vous pouvez regarder l’exemple ci-dessous :

<body>  
    <h1>Titre principal</h1>  
    <p>Un paragraphe</p>  
    <p id='p1'>Paragraphe 1 </p>  
    <p id='p2'>Paragraphe 2 </p> </body>  

<script>
// nos deux paragraphes p id='p1' et p id='p2'  
let para1 = document.getElementById('p1');  
let para2 = document.getElementById('p2');  

/* On utilise la fonction addEventListener() qui sert de gestionnaire 
 * d'évènements. Ici, on demande à exécuter la fonction anonyme passée en 
 * deuxième argument lors de l'évènement "click" (clic) sur l'élément 
 * p id='p1' ou p id='p2' 
 */  
para1.addEventListener('click', function(){alert('Clic sur p id=p1');});  
para2.addEventListener('click', function(){alert('Clic sur p id=p2');});