Closures pour gérer les événements en JavaScript

La fonction addEventListener () permet d'associer une fonction (le gestionnaire d'événement) à un événement. Lorsque l'événement survient, le navigateur appelle le gestionnaire d'événement en lui transmettant un unique argument sous la forme d'un objet décrivant l'événement.
Comment transmettre autre chose à ce gestionnaire, comme par exemple une référence sur un objet spécifique ?

La solution

La solution passe par une closure. Une closure est un ensemble de variables préservées en dehors de la portée du sous-programme où elles sont définies, parce que un ou plusieurs autres sous-programmes y font référence.
Concrètement une fonction f () retourne une fonction g () (ie : un objet Function) telle que g () utilise la variable o transmise en argument à f () comme si c'était une variable globale. Cette variable étant une variable locale de f (), elle devrait être désallouée à la fin de l'appel à f (). Toutefois, comme g () y fait référence, la variable est préservée. Par exemple :
function f (o) {
	return (function g () {
		o.value ++; // g () référence la variable locale o de f () comme si c'était une variable globale
	});
} // Fin de la fonction : o n'est pas désallouée car l'objet Function g () retourné y fait référence
Il suffit donc de fournir à addEventListener () une fonction g () retournée par un appel à f () à laquelle une référence sur un objet est transmise via l'argument o.

Le code JavaScript

En JavaScript, la solution se traduit par le code suivant. Il s'agit d'un exemple dans lequel on dispose d'une liste d'objets contenant trois propriétés : text, color et backgroundColor. Pour chacun de ces objets, on souhaite générer un

contenant le texte de l'objet. Par ailleurs, on souhaite que cet élément HTML prenne la couleur et la couleur de fond de l'objet correspondant quand l'utilisateur clique dessus. Ce qui donne :
var tagDIV, messages, i;

messages = [
	{text:"Bonjour", color:"red", backgroundColor:"blue"},
	{text:"Au revoir", color:"green", backgroundColor:"yellow"}
];
for (i = 0; i != messages.length; i ++) {
	tagDIV = document.createElement ("div");
	tagDIV.innerHTML = messages[i].text;
	tagDIV.addEventListener ("click", function (o) { return (function (e) { this.style.color = o.color; this.style.backgroundColor = o.backgroundColor; }); } (messages[i]), false);
	document.body.appendChild (tagDIV);
}
Ce code se lit ainsi :
  • la fonction anonyme function (o) retourne une fonction anonyme function (e) servant de gestionnaire d'événément (e correspond à l'objet de l'événement) ;
  • function (e) fait référence à l'argument o de function (o) (lequel argument constitue une variable locale de cette fonction) comme s'il s'agissait d'une variable globale ;
  • de ce fait, o est préservée à la fin de l'appel à function (o), et par conséquent l'objet du tableau messages[] de l'itération courante à laquelle cette variable fait référence ;
  • quand l'événement survient, le gestionnaire d'événement function (e) dispose toujours d'une référence valide sur cet objet via o, si bien qu'il peut en accéder aux propriétés.
Cliquez ici pour tester cet exemple. Vous pourrez visualiser le code et le récupérer pour travailler avec.
La petite subtilité à bien saisir : c'est la référence sur la variable o qui est maintenue. On le voit bien quand o est une constante et non une référence sur un objet :
function f (o) {
	return (function g () { alert (o); });
}

var x = 3;

window.setTimeout (f (x), 2000); // Créer un gestionnaire d'événement alors que x vaut 3, et l'appeler dans 2 secondes
window.setTimeout (function () { x = 5; }, 1000); // 1 seconde avant, passer la valeur de x à 5
Dans cet exemple, le gestionnaire d'événement va afficher 3, car o référence la constante 3 et non un objet dont la valeur serait passée de 3 à 5.
Il est possible de simplifier l'écriture en factorisant un peu le code de function (o). A cette occasion, il est intéressant d'introduire un raffinement qui se révèle fort utile quand on souhaite utiliser une fonction d'un objet comme gestionnaire d'événement. Dans le code du gestionnaire d'événement, il s'agit de faire en sorte que this référence l'objet transmis en argument à function (o) plutôt que l'élément HTML à l'origine de l'événement (toujours accessible via e.target).
Ainsi, le code du gestionnaire peut s'écrire comme une fonction d'un objet :
function CMessage (text, color, backgroundColor) {
	this.text = text;
	this.color = color;
	this.backgroundColor = backgroundColor;
}

CMessage.prototype.onClick = function (e) {
	e.target.style.color = this.color;
	e.target.style.backgroundColor = this.backgroundColor;
};
Le coeur de ce système est createEventListener (), produit de la factorisation :
function createEventListener (o, handler) {
	return (function (e) { handler.call (o, e); });
}
Et le code du programme devient :
function run () {
	var tagDIV, messages, i;

	messages = new Array ();
	messages.push (new CMessage ("Bonjour", "red", "blue"));
	messages.push (new CMessage ("Au revoir", "green", "yellow"));
	for (i = 0; i != messages.length; i ++) {
		tagDIV = document.createElement ("div");
		tagDIV.innerHTML = messages[i].text;
		tagDIV.addEventListener ("click", createEventListener (messages[i], CMessage.prototype.onClick), false);
		document.body.appendChild (tagDIV);
	}
}

L'exemple

Cliquez ici pour tester cet exemple. Vous pourrez visualiser le code et le récupérer pour travailler avec.

La logique

La fonction createEventListener () est très courte. Pour autant, son code doit être lu avec soin, car il accomplit beaucoup :
  • La fonction retourne une fonction anonyme (function (e)) qui prend un seul argument. En tant que telle, cette fonction peut être utilisée comme gestionnaire d'un événement HTML, son argument e correspondant alors à cet événement.
  • Quand l'événement survient, la fonction anonyme ainsi utilisé est donc appelée. A son tour, elle appelle le véritable gestionnnaire via call (). Pour rappel, tout objet est doté de call (). En effet, c'est une fonction de Object dont le prototype se retrouve dans la chaîne des prototypes de tout objet créé via l'opérateur new (ou pour le dire moins rigoureusement, mais plus simplement : "dont tout objet hérite").
  • Ce véritable gestionnaire, c'est la fonction référencée par handler de l'objet o. L'appel via call () permet d'imposer la valeur de this dans le code du véritable gestionnaire comme une référence sur l'objet o.
Une version plus élaborée de createEventListener () permet de passer au véritable gestionnaire d'événement d'autres arguments que e. Pour cela, la fonction utilise non plus call (), mais apply () et bind (), deux autres fonctions héritées de Object :
/*------------------------------------------------------------------------------
Crée un gestionnaire d'évènement qui appelera une fonction en lui transmettant
une référence this sur un objet, un référence sur l'évènement et éventuellement
d'autres arguments

ENTREE
	object	Objet sur lequel this pointera dans le corps de la fonction
	handler	Fonction appelée par le gestionnaire d'évènement
	args	Tableau de arguments (optionnel) transmis de plus à la fonction

SORTIE
	Pointeur sur le gestionnaire d'évènement
------------------------------------------------------------------------------*/

function createEventListener (object, handler, args) {
	var f;

	if (args instanceof Array)
		f = function (e) { args.push (e); handler.apply (this, args); args.pop (); };
	else
		f = handler;
	return (f.bind (object));
}
Les arguments supplémentaires doivent être transmis via un tableau. Par exemple, pour transmettre trois arguments "Hello", 1970 et 3.14 au gestionnaire :
var eventHandler;

eventHandler = createEventListener (messages[i], CMessage.prototype.onClick, ["Hello", 1970, 3.14]);
tagDIV.addEventListener ("click", eventHandler, false);
Les gestionnaire d'événément prendra alors la forme suivante :
CMessage.prototype.onClick = function (e, word, year, pi) {
	// Code du gestionnaire
};
Cliquez ici pour tester un exemple. Vous pourrez visualiser le code et le récupérer pour travailler avec.
Closures pour gérer les événements en JavaScript