Apprenez en lisant dans l'ordre

Closures — des fonctions qui gardent en mémoire les variables extérieures

Une closure est une fonction qui garde en mémoire les variables d'une portée extérieure. Compteurs persistants, fabriques de fonctions et fonctions créées en boucle.

Il arrive qu'on veuille confier à une fonction une valeur qui augmente à chaque appel, comme le nombre de vues d'une page. Si tu comptes avec une variable ordinaire, soit la valeur ne se conserve pas, soit n'importe quelle ligne peut l'écraser.

Cet article traite des closures, des fonctions qui gardent en mémoire les variables d'une portée extérieure, et des fabriques de fonctions, des fonctions qui renvoient une fonction avec un réglage déjà intégré.

Conserver une valeur d'un appel à l'autre — les bases des closures

Imagine que tu veuilles compter les téléchargements d'un document. Si tu déclares un let dans la fonction qui compte, il repart de 0 à chaque appel. Si tu le déclares au niveau le plus extérieur, la valeur se conserve, mais elle se retrouve à un endroit où n'importe quelle ligne peut l'écraser.

Avec une closure (une fonction qui garde en mémoire les variables d'une portée extérieure), tu peux conserver une valeur d'un appel à l'autre tout en empêchant les lignes extérieures de l'écraser. Si tu déclares une variable dans une fonction et que tu renvoies avec return une fonction qui la lit et la modifie, la fonction renvoyée emporte cette variable avec elle.

function makeDownloadCounter() {
  let total = 0;                  // Variable qui n'existe que dans makeDownloadCounter

  const countUp = () => {
    total = total + 1;            // Met à jour le total extérieur
    return total;
  };

  return countUp;                 // Renvoie la fonction elle-même
}

const countDownload = makeDownloadCounter();   // Crée un compteur
console.log(countDownload());     // 1
console.log(countDownload());     // 2
console.log(countDownload());     // 3 (reprend à partir de la valeur précédente)
// console.log(total);            // ReferenceError: total is not defined
Variable retenue par la fonction renvoyée
Niveau le plus extérieur (hors de toute accolade)
  • const countDownload = makeDownloadCounter() — contient la fonction renvoyée
  • console.log(total) — le nom n'est pas visible, d'où la ReferenceError
Accolades de makeDownloadCounter
  • let total = 0 — appeler countDownload() ne le remet pas à 0
  • return countUp — renvoie la fonction elle-même, pas une valeur
Accolades de countUp
  • total = total + 1 — met à jour le total extérieur
  • Peut encore lire et modifier ce total une fois la fonction extérieure terminée
countUp est renvoyée au niveau le plus extérieur et garde toujours en mémoire le total extérieur. Même une fois la fonction terminée, la variable gardée en mémoire reste là.

La fonction nommée countUp à l'intérieur est appelée countDownload à l'extérieur. Ce que return transmet, c'est la fonction elle-même : tu peux donc donner à la variable qui la reçoit le nom qui convient à l'endroit où tu l'utilises. Le tableau ci-dessous compare, selon l'endroit où tu places la valeur comptée, si elle se conserve entre les appels et si elle peut être écrasée.

Où placer le compteurValeur d'un appel à l'autreModifiable depuis l'extérieur ?
Déclarer let dans la fonction qui compteDisparaît à la fin de l'appel, repart de 0 la fois suivanteNon, le nom n'est pas visible
Déclarer let au niveau le plus extérieurConservéeOui, depuis n'importe quelle ligne
Déclarer let dans une fonction extérieure et renvoyer une fonction intérieureReprend à partir de la valeur précédente à chaque appel de la fonction renvoyéeSeulement via la fonction renvoyée

Un essai gratuit de vidéo permet jusqu'à trois lectures. Déclare les variables toi-même.

① Écris makeTrialCounter, qui renvoie une fonction : elle part de 3 lectures restantes, retire 1 à chaque appel et renvoie le nombre restant. Ne descends pas en dessous de 0.

② Crée une fonction d'essai avec makeTrialCounter.

③ Appelle la fonction de ② quatre fois, et affiche chaque résultat sous la forme « Lectures restantes : N ».

(Si le code s'exécute correctement, une explication s'affiche.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Créer des fonctions avec un réglage intégré — les fabriques de fonctions

Imagine que le taux de points dépende du rang du membre. Si tu passes le taux en argument à chaque calcul, l'appelant risque de confondre les rangs, et tu finis par écrire le même taux encore et encore. Mieux vaut fixer le taux une bonne fois pour toutes, à l'avance.

Avec une fabrique de fonctions (une fonction qui reçoit un réglage et renvoie une fonction qui le garde en mémoire), tu ne passes le taux qu'une seule fois. Une closure garde aussi en mémoire les arguments, donc chaque fonction renvoyée continue de lire le rate avec lequel elle a été créée.

function makePointCalculator(rate) {
  // rate est un argument. La fonction renvoyée continue de le garder en mémoire
  return (amount) => Math.floor(amount * rate);
}

const regularPoint = makePointCalculator(0.01);    // Membres standard : 1 %
const goldPoint = makePointCalculator(0.05);       // Membres Gold : 5 %
const platinumPoint = makePointCalculator(0.1);    // Membres Platinum : 10 %

console.log(regularPoint(3000));    // 30
console.log(goldPoint(3000));       // 150
console.log(platinumPoint(3000));   // 300

// Sans passer par une variable, calcule sur-le-champ avec deux paires de parenthèses
console.log(makePointCalculator(0.05)(5000));   // 250
Trois fonctions à partir d'une seule définition
Une définition :makePointCalculatorPasse (0.01)→ regularPointPasse (0.05)→ goldPointPasse (0.1)→ platinumPointrate gardéen mémoire = 0.01rate gardéen mémoire = 0.05rate gardéen mémoire = 0.13000 yens→ renvoie 303000 yens→ renvoie 1503000 yens→ renvoie 300
Le corps de chaque fonction est la même expression : la seule différence, c'est le rate que chacune garde en mémoire. Chaque fonction continue de lire le rate que tu lui as passé, sauf si quelque chose le réaffecte.

Dans makePointCalculator(0.05)(5000), la première paire de parenthèses crée une fonction qui garde le taux en mémoire, et la seconde lui passe le montant et calcule sur-le-champ. Avec une seule paire de parenthèses, ce que tu récupères est une fonction, pas un nombre calculé.

Le magasin principal et la succursale n'ont pas le même seuil de livraison gratuite ni les mêmes frais de port. cartTotals est déjà déclaré.

① Écris makeShippingFee, qui reçoit le seuil de livraison gratuite et les frais, et renvoie une fonction qui reçoit le total du panier et renvoie les frais de port.

② Crée les fonctions du magasin principal (gratuit à partir de 4000 yens, sinon 500 yens) et de la succursale (gratuit à partir de 3000 yens, sinon 350 yens).

③ Passe chaque montant de cartTotals dans les deux fonctions, et affiche « Panier de N yens → frais : magasin principal N, succursale N ».

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Une nouvelle variable à chaque fois — fabriques et let

Imagine que tu veuilles décompter le stock restant de chaque produit. Si tu prépares une seule variable partagée pour la soustraction, réserver des t-shirts entame aussi le stock de casquettes. Chaque produit a besoin de sa propre valeur.

Comme avec makeDownloadCounter au début de cet article, si tu déclares un let dans une fonction qui renvoie une fonction, cette variable est recréée à chaque appel de la fonction extérieure. Les fonctions renvoyées ne partagent pas de variables : même si elles viennent de la même définition, chacune ne lit et ne modifie que la sienne.

function makeStockReserver(stock) {
  let rest = stock;               // Recréé à chaque appel

  return (count) => {
    rest = rest - count;
    return rest;
  };
}

const reserveShirt = makeStockReserver(10);
const reserveCap = makeStockReserver(4);

console.log(reserveShirt(3));     // 7
console.log(reserveCap(1));       // 3
console.log(reserveShirt(2));     // 5 (seul le stock de t-shirts diminue)
console.log(reserveCap(1));       // 2
Une nouvelle variable à chaque appel
makeStockReserver(10) s'exécuteCrée une variablerest = 10makeStockReserver(4) s'exécuteCrée un autrerest = 4, séparéreserveShirt(3) etreserveCap(1)Renvoient 7 et 3,indépendamment
Même à partir de la même définition, chaque fonction reçoit son propre rest. Diminuer l'un ne touche pas au stock restant de l'autre.

reserveShirt(2), dans le troisième console.log, ne fait baisser que le stock de t-shirts, de 7 à 5, et le côté casquettes reste à 3. Le reserveCap(1) suivant renvoie 2, ce qui montre que la fonction des casquettes garde elle aussi son propre compte.

Appeler la fabrique à chaque fois fait tout repartir de zéro

Si tu écris deux fois makeStockReserver(10)(3), les deux renvoient 7, parce que chaque appel crée un rest séparé qui part de 10. Pour que la réservation suivante parte du stock déjà diminué, place la fonction renvoyée dans une variable comme reserveShirt, et appelle cette variable.

Génère des ID séquentiels pour les commandes et les retours, chacun avec sa propre numérotation. Déclare les variables toi-même.

① Écris makeIdIssuer, qui reçoit un préfixe et renvoie une fonction qui, à chaque appel, renvoie l'ID suivant : « préfixe-1 », « préfixe-2 », etc.

② Crée une fonction de génération pour ORD et une pour RTN.

③ Génère deux ID de commande et un ID de retour, et affiche-les.

④ Génère un ID de commande de plus, affiche-le, et vérifie que la numérotation reprend là où elle s'était arrêtée.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

Créer des fonctions dans une boucle — un let distinct à chaque tour

Imagine que tu veuilles préparer d'un coup plusieurs fonctions d'affichage, une par niveau de log. Si tu crées des fonctions dans une boucle et que tu les ranges dans un tableau, tu ne les appelles qu'une fois la boucle terminée. La valeur que chaque fonction lit alors pour son tour dépend de la façon dont tu as déclaré la variable de boucle.

Ce cours n'utilise pas var, mais le mettre à côté de let rend la différence évidente. Avec let dans les parenthèses du for, une nouvelle variable est créée à chaque tour, et une fonction créée pendant ce tour garde cette variable en mémoire. var partage une seule variable entre tous les tours, donc chaque fonction lit la valeur laissée après la boucle.

const levels = ["info", "warn", "error"];

// var partage une seule variable entre tous les tours
const varLoggers = [];
for (var i = 0; i < levels.length; i++) {
  varLoggers.push(() => `[${levels[i]}]`);
}
console.log(varLoggers[0]());     // [undefined]

// let crée une nouvelle variable à chaque tour
const letLoggers = [];
for (let j = 0; j < levels.length; j++) {
  letLoggers.push(() => `[${levels[j]}]`);
}
console.log(letLoggers[0]());     // [info]
console.log(letLoggers[2]());     // [error]
Ce que lisent les fonctions qui gardent une variable de boucle
var dans lesparenthèses du forUne seule variablesur tous les toursLes 3 fonctionslisent le même iRenvoie[undefined]let dans lesparenthèses du forUne nouvellevariable par tourChacune garde0, 1 ou 2Renvoie[info]
Côté var, les trois fonctions lisent la même variable ; côté let, chacune lit la valeur de son propre tour. Ce qu'une fonction garde en mémoire dépend du fait qu'une nouvelle variable soit créée ou non à chaque tour.

Le côté var donne [undefined] parce que i vaut 3 une fois sorti de la boucle, et que levels[3] n'existe pas. Côté let, levels[j] est lui aussi lu au moment de l'appel, mais le j lu est la variable créée pour ce tour-là, donc la valeur de chaque tour est toujours là.

Une closure ne mémorise pas la valeur au moment de sa création

Ce qu'une closure garde en mémoire, c'est la variable elle-même, et elle lit la valeur que contient la variable au moment de l'appel. Si tu crées une fonction qui lit let rate = 0.05; puis que tu réaffectes rate = 0.1;, cette fonction calcule elle aussi avec 0.1. Pour un réglage que tu veux figer, passe-le en argument de la fabrique, comme dans makePointCalculator(0.05).

Crée pour chaque moyen de paiement une fonction qui affiche un avis de confirmation. methods, labels et notifiers sont déjà déclarés.

① Parcours methods avec une boucle for en mettant let dans ses parenthèses.

② À chaque tour, utilise le nom du moyen de paiement comme clé, et place dans notifiers une fonction qui reçoit un numéro de commande et affiche un avis comme « [Virement bancaire] Commande A-1002 reçue ».

③ Après la boucle, passe A-1002 à bank et A-1003 à cod.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Si tu appelles deux fois countDownload() puis que tu affiches total au niveau le plus extérieur, que se passe-t-il ?

Question 2reserveCap démarre avec un stock de 4. Après que reserveShirt a retiré 3, tu passes 1 à reserveCap. Que renvoie-t-elle ?

Question 3Si tu appelles une fonction créée dans for (var i = 0; ...) une fois la boucle terminée, que vaut i ?