Scope et Closures en JavaScript – Expliqué avec des exemples
Vous avez peut-être déjà rencontré ou écrit du code similaire à celui-ci en écrivant du JavaScript :
function sayWord(word) {
return () => console.log(word) ;
}
const sayHello = sayWord("hello") ;
sayHello() ; // "bonjour"
Ce code est intéressant pour plusieurs raisons. Premièrement, nous pouvons accéder à word
dans la fonction renvoyée par sayWord
. Deuxièmement, nous avons accès à la valeur de word
lorsque nous appelons sayHello
– même si nous appelons sayHello
là où nous n’avons pas accès à word
.
Dans cet article, nous allons découvrir les notions de scope et de closures, qui permettent ce comportement.
Présentation de Scope en JavaScript
La portée est le premier élément qui nous aidera à comprendre l’exemple précédent. La portée d’une variable est la partie du programme dans laquelle elle est disponible pour être utilisée.
Les variables JavaScript ont une portée lexicale, ce qui signifie que nous pouvons déterminer la portée d’une variable à partir de l’endroit où elle est déclarée dans le code source. (Ce n’est pas tout à fait vrai : les variables var
n’ont pas de portée lexicale, mais nous en parlerons bientôt)
Prenons l’exemple suivant :
if (true) {
const foo = "foo" ;
console.log(foo) ; // "foo"
}
L’instruction if
introduit une portée de bloc en utilisant une instruction de bloc. Nous disons que foo
a une portée de bloc dans l’instruction if
. Cela signifie qu’on ne peut y accéder qu’à l’intérieur de ce bloc.
Si nous essayons d’accéder à foo
en dehors du bloc, nous obtenons une ReferenceError
car il est hors de portée :
if (true) {
const foo = "foo" ;
console.log(foo) ; // "foo"
}
console.log(foo) ; // Erreur de référence non attrapée : foo n'est pas défini
Les instructions de bloc sous d’autres formes, telles que les boucles for
et while
, créent également une portée pour les variables de bloc. Par exemple, foo
a une portée dans le corps d’une fonction ci-dessous :
function sayFoo() {
const foo = "foo" ;
console.log(foo) ;
}
sayFoo() ; // "foo"
console.log(foo) ; // Erreur de référence non attrapée : foo n'est pas défini
Portées et fonctions imbriquées
JavaScript autorise les blocs imbriqués et donc les portées imbriquées. Les portées imbriquées créent un arbre de portée ou une chaîne de portée.
Considérez le code ci-dessous, qui imbrique plusieurs instructions de bloc :
if (true) {
const foo = "foo" ;
console.log(foo) ; // "foo"
if (true) {
const bar = "bar" ;
console.log(foo) ; // "foo"
if (true) {
console.log(foo, bar) ; // "foo bar
}
}
}
JavaScript nous permet également d’imbriquer des fonctions :
function foo(bar) {
function baz() {
console.log(bar) ;
}
baz() ;
}
foo("bar") ; // "bar"
Comme prévu, nous pouvons accéder aux variables depuis leur portée directe (la portée où elles sont déclarées). Nous pouvons également accéder aux variables à partir de leurs portées internes (les portées qui s’imbriquent dans leur portée directe). En d’autres termes, nous pouvons accéder aux variables à partir de la portée dans laquelle elles sont déclarées et à partir de chaque portée interne.
Avant d’aller plus loin, nous devons clarifier la différence de comportement entre les types de déclaration de variables.
Portée de let, const et var en JavaScript
Nous pouvons créer des variables à l’aide des déclarations let
, const
et var
. Pour let
et const
, la portée des blocs fonctionne comme expliqué ci-dessus. Cependant, var
se comporte différemment.
let et const
let
et const
créent des variables à portée de bloc. Lorsqu’elles sont déclarées dans un bloc, elles ne sont accessibles que dans ce bloc. Ce comportement a été démontré dans nos exemples précédents :
if (true) {
const foo = "foo" ;
console.log(foo) ; // "foo"
}
console.log(foo) ; // Uncaught ReferenceError : foo n'est pas défini
var
Les variables créées avec var
sont limitées à la fonction la plus proche ou à la portée globale (dont nous parlerons bientôt). Elles n’ont pas de portée de bloc :
function foo() {
if (true) {
var foo = "foo" ;
}
console.log(foo) ;
}
foo() ; // "foo"
var
peut créer des situations confuses, et cette information n’est incluse que par souci d’exhaustivité. Il est préférable d’utiliser let
et const
lorsque cela est possible. Le reste de cet article ne concerne que les variables let
et const
.
Si vous souhaitez savoir comment var
se comporte dans l’exemple ci-dessus, vous devriez consulter mon article sur le hissage.
Portée globale et portée des modules en JavaScript
En plus de la portée des blocs, les variables peuvent avoir une portée globale ou modulaire.
Dans un navigateur Web, la portée globale se situe au niveau supérieur d’un script. Il s’agit de la racine de l’arbre de portée que nous avons décrit précédemment, et elle contient toutes les autres portées. Ainsi, la création d’une variable dans la portée globale la rend accessible dans toutes les portées :
Chaque module possède également sa propre portée. Les variables déclarées au niveau du module ne sont disponibles que dans ce module – elles ne sont pas globales :
Les fermetures en JavaScript
Maintenant que nous comprenons la portée, revenons à l’exemple que nous avons vu dans l’introduction :
function sayWord(word) {
return () => console.log(word) ;
}
const sayHello = sayWord("hello") ;
sayHello() ; // "bonjour"
Rappelez-vous qu’il y avait deux points intéressants dans cet exemple :
- La fonction retournée par
sayWord
peut accéder au paramètreword
- La fonction renvoyée conserve la valeur de
word
lorsquesayHello
est appelée en dehors de la portée deword
Le premier point peut être expliqué par la portée lexicale : la fonction retournée peut accéder à word
car il existe dans sa portée externe.
Le second point est dû aux fermetures : Une fermeture est une fonction combinée avec des références aux variables définies à l’extérieur de celle-ci. Les fermetures maintiennent les références aux variables, ce qui permet aux fonctions d’accéder aux variables en dehors de leur portée. Elles « enferment » la fonction et les variables dans son environnement.
Exemples de fermetures en JavaScript
Vous avez probablement rencontré et utilisé fréquemment des fermetures sans en être conscient. Explorons maintenant d’autres façons d’utiliser les fermetures.
Callbacks
Il est courant qu’une callback fasse référence à une variable déclarée en dehors d’elle-même. Par exemple :
function getCarsByMake(make) {
return cars.filter(x => x.make === make) ;
}
make
est disponible dans la callback en raison du scoping lexical, et la valeur de make
est conservée lorsque la fonction anonyme est appelée par filter
en raison d’une fermeture.
Stockage de l’état
Nous pouvons utiliser les fermetures pour renvoyer des objets à partir de fonctions qui stockent un état. Considérons la fonction makePerson
suivante qui renvoie un objet pouvant stocker et modifier un nom
:
function makePerson(name) {
let _nom = nom ;
return {
setName : (newName) => (_name = newName),
getName : () => _name,
} ;
}
const me = makePerson("Zach") ;
console.log(me.getName()) ; // "Zach"
me.setName("Zach Snoek") ;
console.log(me.getName()) ; // "Zach Snoek"
Cet exemple montre que les fermetures ne se contentent pas de geler les valeurs des variables de la portée externe d’une fonction lors de sa création. Au contraire, elles maintiennent les références pendant toute la durée de vie de la fermeture.
Méthodes privées
Si vous êtes familier avec la programmation orientée objet, vous avez peut-être remarqué que notre exemple précédent ressemble beaucoup à une classe qui stocke un état privé et expose des méthodes publiques getter et setter. Nous pouvons étendre ce parallèle orienté objet en utilisant les fermetures pour implémenter des méthodes privées :
function makePerson(name) {
let _name = name ;
function privateSetName(newName) {
_nom = nouveauNom ;
}
return {
setName : (nouveauNom) => privateSetName(nouveauNom),
getName : () => _nom,
} ;
}
privateSetName
n’est pas directement accessible aux consommateurs et il peut accéder à la variable d’état privée _name
par le biais d’une fermeture.
Gestionnaires d’événements React
Enfin, les fermetures sont courantes dans les gestionnaires d’événements de React. Le composant Counter
suivant a été modifié à partir de la documentation de React:
function Counter({ initialCount }) {
const [count, setCount] = React.useState(initialCount) ;
return (
<>
) ;
}
function App() {
return ;
}
Les fermetures permettent de :
- les gestionnaires de clics des boutons reset, decrement, et increment pour accéder à
setCount
- le bouton de réinitialisation pour accéder à
initialCount
depuis les props deCounter
- et le bouton « Show count » pour afficher l’état du
comptage
.
Les fermetures sont importantes dans d’autres parties de React, comme les props et les hooks. La discussion sur ces sujets est hors de portée pour cet article. Je vous recommande de lire ce billet de Kent C. Dodds ou ce billet de Dan Abramov pour en savoir plus sur le rôle que jouent les closures dans React.
Conclusion
La portée fait référence à la partie d’un programme où nous pouvons accéder à une variable. JavaScript nous permet d’imbriquer les scopes, et les variables déclarées dans les scopes extérieurs sont accessibles depuis tous les scopes intérieurs. Les variables peuvent avoir une portée globale, de module ou de bloc.
Une fermeture est une fonction entourée de références aux variables dans sa portée externe. Les fermetures permettent aux fonctions de maintenir des connexions avec les variables externes, même en dehors de la portée des variables.
Les fermetures sont utilisées à de nombreuses fins, qu’il s’agisse de créer des structures de type classe qui stockent l’état et mettent en œuvre des méthodes privées ou de transmettre des rappels aux gestionnaires d’événements.
Connectons nous
Si d’autres articles comme celui-ci vous intéressent, abonnez-vous à ma newsletter et connectez-vous avec moi sur LinkedIn et Twitter!
Remerciements
Merci à Bryan Smith d’avoir fourni des commentaires sur les versions préliminaires de ce billet.
Photo de couverture par Karine Avetisyan sur Unsplash.
Laisser un commentaire