Question 1Si tu appelles deux fois countDownload() puis que tu affiches total au niveau le plus extérieur, que se passe-t-il ?
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
const countDownload = makeDownloadCounter()— contient la fonction renvoyéeconsole.log(total)— le nom n'est pas visible, d'où laReferenceError
let total = 0— appelercountDownload()ne le remet pas à 0return countUp— renvoie la fonction elle-même, pas une valeur
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 compteur | Valeur d'un appel à l'autre | Modifiable depuis l'extérieur ? |
|---|---|---|
| Déclarer let dans la fonction qui compte | Disparaît à la fin de l'appel, repart de 0 la fois suivante | Non, le nom n'est pas visible |
| Déclarer let au niveau le plus extérieur | Conservée | Oui, depuis n'importe quelle ligne |
| Déclarer let dans une fonction extérieure et renvoyer une fonction intérieure | Reprend à partir de la valeur précédente à chaque appel de la fonction renvoyée | Seulement via la fonction renvoyée |
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
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é.
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
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.
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]
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).
Vérification des connaissances
Répondez à chaque question une par une.
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 ?