Kergoz Panic

par
 
Watilin
le
 
10/08/2012 à 12:21
catégorie
 
Informatique

Les fermetures en JavaScript

Si le président de la République était un interpréteur JavaScript, les variables auraient le droit de picoler dans la rue. C’est le principe de la fermeture.

Pour être plus précis, la fermeture est un concept de la logique formelle utilisé par JavaScript, que j’ai découvert il y a quelques jours. Je trouve ça hyper puissant, et c’est encore une fois un aspect souvent inconnu du langage. Je vais essayer de vous en expliquer les grandes lignes…

Introduction

Tout a commencé alors que je flânais sur Wikipedia comme ça m’arrive parfois. Sur la page consacrée à JavaScript, à la rubrique particularités du langage, un bout de code laconique attire mon attention. Ça parlait de fermetures (closure en anglais). J’ai d’abord cru à une obscure erreur de traduction. Puis je me suis rappelé un passage de ce livre de Christophe Porteneuve (toujours le même ^^) qui évoquait la fermeture lexicale. Piqué de curiosité, j’ai finalement réatteri sur le Wiki.

Il s’agit bien du même concept. En logique formelle, il est lié à celui de variable libre. Sans entrer dans les détails, une variable libre, dans une expression, est une variable qui n’est pas fixée : elle fait référence à quelque chose d’extérieur. La fermeture consiste à fixer une valeur à chaque variable libre au moment d’évaluer l’expression, en utilisant le contexte extérieur.

I. Comment ça marche

Les fermetures sont possibles en JavaScript grâce à plusieurs mécanismes.

I.1. JavaScript est un langage de premier ordre

Le premier ordre est encore un terme de la logique formelle. Dans un langage de premier ordre, les fonctions peuvent être manipulées de la même manière que les autres types de données comme int ou string. Ce mécanisme permet à lui seul des tas de choses aussi déroutantes qu’incroyables, comme assigner une fonction à une variable pour l’appeler plus tard, ou encore la passer en paramètre à une autre fonction. Par exemple :

// do est une fonction « ordinaire »
function do(f) {
	f();
}

// sayHello va être appelée à travers do
function sayHello() {
	alert("Hello!");
}

// voici l’appel
do(sayHello);

Ce principe est utilisé par de nombreuses frameworks, ainsi que par les gestionnaires d’évènements du DOM. On « écoute » un évènement en lui assignant une fonction de rappel, qui sera appelée à chaque fois que l’évènement se déclenche :

// fonction qui modifie le style CSS d’un élément donné
// (quand la fonction est appelée par un évènement,
// this désigne l’élément qui a déclenché cet évènement)
function toggleColor() {
	if (this.style.color == "green")
		this.style.color = "red";
	else
		this.style.color = "green";
}

// toggleColor sera appelée à chaque clic sur l’élément machin
machin.addEventListener("click", toggleColor, false);

I.2. JavaScript autorise les fonctions anonymes

Une fonction anonyme est une fonction qui n’a pas de nom. On le fait tous les jours sans s’en rendre compte avec les nombres. Prenez une bête opération d’addition ; on peut passer par des variables, comme ceci :

var x = 3, y = 5;
var z = x + y;

Ou bien, si on n’a pas l’intention de se resservir des nombres :

var z = 3 + 5;

Ce sont des nombres anonymes ! Pour les fonctions anonymes, c’est exactement pareil, on peut les déclarer au moment de les utiliser – on dit « à la volée » – sans passer par une variable. Reprenons la fonction do de tout à l’heure :

do( function() { alert("Ceci est une fonction anonyme."); } );

D’ailleurs, il y a une syntaxe alternative de déclaration de fonction qui met en évidence à la fois que les fonctions peuvent être anonymes et référencées par de simples variables :

var bidule = function() {
	alert("Les Smarties bleus ont disparu");
}
bidule();

C’est un moyen bien pratique de définir une fonction de rappel.

machin.addEventListener("click", function() {
	if (this.style.color == "green")
		this.style.color = "red";
	else
		this.style.color = "green";
}, false);

I.3. JavaScript permet d’imbriquer les définitions de fonctions

En réalité, il s’agit du même mécanisme que le premier ordre, mais vu sous ce point de vue, ça va nous permettre de bien voir comment se produit une fermeture. On l’a vu, comme les fonctions sont des variables comme les autres, il n’y a pas de raison qu’on ne puisse pas en définir localement.

Dans un langage d’ordre zéro comme C, C++ et Java parmi les plus connus, toutes les fonctions sont définies au même niveau de visibilité, dans la même classe ou au niveau global (bien sûr, il y a moyen de tricher avec les pointeurs de fonction, mais ça n’est pas du premier ordre). En revanche, en JavaScript, une fonction locale disparaît, comme les autres variables locales, dès que la fonction parente se termine ; elle n’est donc pas visible des autres fonctions.

function bigMess(x) {
	// steakHache n’est pas visible depuis l’extérieur
	function steakHache(y) {
		return (y * 3) mod 7;
	}
	return steakHache(x + 2);
}

Jusqu’ici, croyez-moi, c’est simple. Non, ce qui est vraiment compliqué, c’est qu’en JavaScript, il y a moyen de trafiquer la visibilité des variables ! Et ce de deux manières :

Voilà comment on peut extraire une fonction de son contexte local. À présent, considérons le cas où notre fonction côtoie d’autres variables locales. Un exemple simple : l’auto-incrément, une fonction qui renvoie un entier qui augmente de 1 à chaque appel. Concrètement, nous voulons une fonction que nous appelerons inc et qui se comporte comme ceci :

inc(); // retourne 0
inc(); // retourne 1
inc(); // retourne 2
inc(); // retourne 3
// etc.

Sans fermetures, comment le feriez-vous ? Sûrement avec une variable globale. Avec une fermeture, voici à quoi ça ressemble :

function makeInc() {
	var x = 0;
	return function() {
		return x++;
	}
}

var inc = makeInc();

Ça marche, je vous laisse tester. Mais par quel miracle ?… À la dernière ligne, au moment où on crée la variable/fonction inc, celle-ci emporte avec elle, en quelques sortes, les variables qu’il y a autour, en l’occurence x. Il se crée donc une sorte d’objet invisible autour de la fonction, qui contient cette variable. C’est cet objet qu’on nomme fermeture. Notez que chaque copie de la fonction aura sa propre fermeture :

var inc1 = makeInc();
var inc2 = makeInc();

inc1(); // 0
inc1(); // 1
inc1(); // 2
inc2(); // 0
inc1(); // 3
inc2(); // 1
inc2(); // 2

Vous savez tout. Et là vous allez me dire : c’est beaucoup de mal pour pas grand chose… Détrompez-vous ! Les fermetures se révèlent très utiles dans bien des cas.

II. À quoi ça sert

II.1. Simuler des variables privées

Comme je vous l’ai dit, la fermeture est une sorte d’objet. Comme des objets, elles reprennent le principe de l’encapsulation, c’est-à-dire qu’elles protègent leurs variables en les enveloppant. En utilisant ce principe, on peut donc reproduire un mécanisme de variables privées, avec getter et setter, en JavaScript :

// variables globales
var getX, setX;

function capsule() {

	var x;

	getX = function() {
		return x;
	};

	setX = function( val ) {
		x = val;
	};

};
// j’exécute la fonction pour créer la fermeture
capsule();

Comme vous pouvez le voir, il n’y a pas moyen de manipuler x directement : elle reste une variable locale de la fonction capsule. Cependant, je peux la manipuler grâce aux deux fonctions que j’ai assignées à des variables globales, et ce mécanisme m’offre tous les avantages des accesseurs habituels : la journalisation, le contrôle de valeur, etc.

Notez toutefois que c’est un peu blasphématoire de prétendre qu’on fait de l’objet avec cette technique : d’une part, ça n’en a pas la syntaxe, et d’autre part, même si on rattache les accesseurs à un autre objet, ça reste du bricolage. D’ailleurs, il y a un vrai mécanisme d’objets en JavaScript, mais ce n’est pas le sujet de cet article.

II.2. Éviter les conflits de noms de variables

Pour éviter les conflits de noms de variables, on utilise couramment les espaces de noms, et c’est une pratique conseillée, sinon obligatoire, dans de nombreux langages. En JavaScript, les espaces de noms sont des objets comme les autres :

var A = {
	x: 0,
	y: 12
};
var B = {
	x: 7,
	y: 14
};

Naturellement, A.x et B.x ne sont pas la même variable.

Cependant, si on veut simplement exécuter du script, sans avoir besoin de conserver les variables pour la suite, on peut utiliser une fonction anonyme comme fermeture. Cela donne une syntaxe un peu étrange. Accrochez-vous :

(function() {

// exemple  : je rajoute du HTML dans la page
var paragraph = document.createElement("p");
document.body.appendChild(paragraph);

})();

Explications : les deux lignes de code au milieu sont tout à fait banales ; en revanche, la fonction qui est autour est exécutée à la volée. Regardez les parenthèses () à la fin. Et pour pouvoir faire un truc pareil, il faut que la fonction anonyme soit elle-même entourée de parenthèses.

Dans cette fonction anonyme, j’utilise une variable locale, paragraph. Elle n’est pas visible depuis l’extérieur et, comme je ne l’ai pas fait sortir du contexte avec un des trucs de sorcier tordu que je vous ai appris plus haut, elle est totalement inaccessible. C’est un excellent moyen de prévention contre les conflits de noms ou les maladresses, mais aussi contre les attaques de type XSS : vos variables sont protégées, personne ne peut les modifier pour changer le comportement de votre script.

Il existe une variante :

(function() {

// …

}());

Remarquez bien la permutation des parenthèses. La différence entre les deux variantes est assez compliquée à expliquer, car elle est liée à la façon dont le code est lu par l’analyseur lexical. Dans les deux cas, la fonction est considérée comme une expression, mais cette expression n’est pas évaluée au même moment. Souvenez-vous simplement qu’il faut deux paires de parenthèses : une autour de la fonction, et une après.

Bien, je crois que je n’ai rien oublié. Vous possédez à présent l’incroyable puissance des fermetures… Mais, pour paraphraser Stan Lee, with great power comes great responsibility, et je vais devoir vous apprendre à vous méfier de ce pouvoir, car il recèle des dangers cachés.

À suivre, section III. : les fuites de mémoire…

III. Les fuites de mémoire

Pour casser tout de suite le mythe, je précise que ce problème n’existe que sous MSIE 6. Il n’est pas directement lié aux fermetures, mais la présence de ces dernières rend son apparition plus probable. Vous allez comprendre…

Présentons les choses simplement. Dans tous les navigateurs Internet supportant JavaScript, la mémoire est partagée en deux espaces distincts. L’un, en gros, contient les objets du DOM, tandis que l’autre est reservé aux objets JavaScript.

Commentaires

darthroudoudou, 2011-02-24 09:19:45

Merci pour ces infos!
je me documentais sur les fermetures, ça m'a été bien utile!

yannick, 2013-02-28 20:23:35

oui, clar, bien structuré, très intéressant, à vous lire, j'ai eu l'impression de bien avancer, merci ..

gaia, 2013-03-08 16:17:45

vu sous cet angle, je comprends un peu mieux l'utilité de JavaScript.

A quand le prochain article.
PS : J'aime beaucoup votre site => PHP évidement... ;D

Page :

Ajouter un commentaire

Anti-bot provisoire