Les closures sont un modèle de programmation en JavaScript qui permet d'utiliser des variables issues d'une portée lexicale extérieure à l'intérieur d'un bloc de code imbriqué. JavaScript prend en charge les closures de manière transparente, et on les utilise souvent sans même savoir ce que c'est.
// Top-level declarations are global-scope
const dozen = 12;
{
// Braces create a new block-scope
// Referencing the outer variable is a closure.
const twoDozen = dozen * 2;
}
// Because of the block-scope declaration, twoDozen is not available here.
twoDozen;
// => Uncaught ReferenceError: twoDozen is not defined
Outre les accolades {}, les fonctions (et les classes) créent elles aussi de nouvelles portées et peuvent enfermer des valeurs :
const dozen = 12;
// Functions create a new function-scope and block-scope.
// Referencing the outer variable here is a closure.
function nDozen(n) {
// This is declared inside the function scope, and uses the top-level scope.
// This works, and encloses the value 12.
const twoDozen = dozen * 2;
// This only uses the locally declared variable and the passed argument to the parameter `n`
return (twoDozen / 2) * n;
}
// Because of the function-scope declaration, twoDozen is not available here.
twoDozen;
// => Uncaught ReferenceError: twoDozen is not defined
Comme le montrent les exemples avec twoDozen, on peut enfermer des valeurs dans une portée imbriquée (fonction, bloc, etc.), mais on ne peut pas les extraire de ce contexte.
Dans la majorité des cas, le JavaScript moderne fait en sorte qu'une valeur ne fuie pas vers une portée extérieure.
Utiliser une déclaration de variable modifiable (comme let ou var) permet de conserver un certain état :
let counter = 0;
// This function closure increments the counter's state in the outer lexical context.
// This way the counter can be shared between many calling contexts.
export function increment() {
counter += 1;
return counter;
}
increment();
// => 1
counter;
// => 1
En combinant les deux idées, à savoir enfermer une valeur pour conserver un état et empêcher les valeurs enfermées de fuiter vers l'extérieur, il est possible de créer des valeurs privées.
La méthode la plus courante consiste à créer une fonction qui renvoie une fonction qui enferme un certain état.
export function makeCounter() {
let counter = 0;
// This returns a new function that encloses the local variable counter
return function increment() {
counter += 1;
return counter;
};
}
// Counter did not leak
counter;
// => Uncaught ReferenceError: counter is not defined
// This creates a new counter.
// This assigns the increment function to the variable myFirstCounter.
const myFirstCounter = makeCounter();
typeof myFirstCounter;
// => function
myFirstCounter.name;
// => increment
myFirstCounter();
// => 1
myFirstCounter();
// => 2
// This creates new counter (with new, separate local state / enclosed counter variable)
const mySecondCounter = makeCounter();
mySecondCounter();
// => 1
// It does not affect the first counter.
myFirstCounter();
// => 3
Beaucoup de programmeurs trouvent les closures difficiles à appréhender, et renvoyer une fonction depuis une fonction n'est pas courant, voire impossible dans certains langages de programmation. Si tu veux de la lecture supplémentaire, le guide sur MDN consacré aux closures est assez complet.
Ton entreprise de design a surtout travaillé avec des transformations CSS pour construire des pages web. Après quelques discussions, la décision est prise de commencer à utiliser JavaScript pour effectuer certains calculs de façon dynamique. Certains de tes collègues sont moins expérimentés avec JavaScript, tu décides donc d'utiliser une fermeture pour créer une transformation réutilisable pour les paires de coordonnées {x, y}.
Implémente la fonction translate2d qui renvoie une fonction utilisant une fermeture pour effectuer une translation 2D répétable d'une paire de coordonnées.
En géométrie, la translation désigne le déplacement de points, de vecteurs ou de formes de la même distance dans une même direction. On peut l'interpréter comme l'addition d'une constante à chaque point.
const moveCoordinatesRight2Px = translate2d(2, 0);
const result = moveCoordinatesRight2Px(4, 8);
// result => [6, 8]
Implémente la fonction scale2d qui renvoie une fonction utilisant une fermeture pour effectuer une mise à l'échelle 2D répétable d'une paire de coordonnées.
En géométrie, la mise à l'échelle uniforme consiste à agrandir ou à réduire des vecteurs ou des formes dans la même direction. On peut l'interpréter comme la multiplication de chaque point par une constante (le facteur d'échelle).
Pour cet exercice, considère que seules des valeurs d'échelle positives sont utilisées.
const doubleScale = scale2d(2, 2);
const result = doubleScale(6, -3);
// result => [12, -6]
Combine deux fonctions de transformation pour effectuer une transformation répétable. C'est ce qu'on appelle souvent la composition de fonctions, où le résultat de la première fonction 'f(x)' sert d'entrée à la seconde fonction 'g(x)'.
const moveCoordinatesRight2Px = translate2d(2, 0);
const doubleCoordinates = scale2d(2, 2);
const composedTransformations = composeTransform(
moveCoordinatesRight2Px,
doubleCoordinates,
);
const result = composedTransformations(0, 1);
// result => [4, 2]
Implémente la fonction memoizeTransform. Elle prend une fonction à mémoïser, puis renvoie une nouvelle fonction qui retient les entrées fournies à cette fonction, afin que la dernière valeur de retour puisse être « mémorisée » et ne soit calculée qu'une seule fois lorsqu'on l'appelle à nouveau avec les mêmes arguments.
La mémoïsation est parfois utilisée en programmation dynamique. Elle permet d'effectuer des opérations coûteuses une seule fois, puisque leurs résultats sont mémorisés. Remarque : dans cet exercice, seul le dernier résultat est mémorisé, contrairement à certaines solutions de programmation dynamique qui mémorisent tous les résultats.
const tripleScale = scale2d(3, 3);
const memoizedScale = memoizeTransform(tripleScale);
memoizedScale(4, 3); // => [12, 9], this is computed since it hasn't been computed before for the arguments
memoizedScale(4, 3); // => [12, 9], this is remembered, since it was computed already
Inscris-toi sur Exercism pour apprendre et maîtriser JavaScript avec 37 concepts159 exercices, et un vrai mentorat humain, le tout gratuitement.