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 wordlorsque 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.

Voir aussi :  Comment utiliser le chaînage optionnel en JavaScript

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 :

  1. La fonction retournée par sayWord peut accéder au paramètre word
  2. La fonction renvoyée conserve la valeur de word lorsque sayHello est appelée en dehors de la portée de word

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.

Voir aussi :  Les meilleurs plugins Webstorm pour un codage efficace

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.

Voir aussi :  Comment créer votre propre extension Google Chrome

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 de Counter
  • 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.