Sortir d’une mauvaise liaison en Javascript.

Note du traducteur : artwaï va essayer de traduire les articles anglophones ne trouvant pas de traduction française qui traite des technologies Web et des concepts que nous essayons de respecter. Sortir d’un mauvaise liaison en Javascript est la première traduction d’une, je l’espère, longue série. -Jean-Baptiste alias Juan

Sortir d’un mauvaise liaison en Javascript.

Illustration Kevin Cornell

La plupart des développeurs ne connaisent pas, ou ne se soucient pas, des liaisons, (aussi appelées « binding ») en javascript. Pourtant ceci est responsable d’un grande partie des questions posées sur les canaux d’aide JavaScript et de milliers, si ce n’est millions de cheveux, arrachés des têtes de développeurs tous les jours. Cependant, avec un peu de considération pour ce sujet souvent dénigré, vous pourriez éviter de perdre votre temps, énergie et patience pour évoluer vers des scripts plus puissants et plus efficaces.

Pourquoi me soucier du binding?

Quasiment aucun langage de programation orientée objet (POO) ne vous oblige à prendre en compte le binding. C’est à dire, il ne requiert pas de vous que vous qualifiez l’accès aux membres (methodes et propriétés) de l’objet courant avec une référence comme this ou self. Si vous appelez une méthode liée à aucun objet spécifiquement, vous appelez en général implicitement l’objet courant. Il en va de même quand vous passez une méthode pour un appel ultérieur : elle conserve sa référence à l’objet auquel elle appartient. En clair, pour la majorité des langages de POO, les liaisons sont implicites. Ceci est vrai en Java, C#, Ruby, Delphi, and C++, pour n’en nommer que quelques uns.

PHP et JavaScript nécessitent que vous indiquiez explicitement l’objet que vous accédez, même si c’est l’objet courant. (Et c’est à peu près le seul point commun que je vois entre PHP et JavaScript.)

Bien sûr, ni PHP ni JavaScript ne sont réellement orientés objets dans le sens traditionnel. Dans le cas de PHP, le support objet a été ajouté après coup, et un peu rapidement; même en PHP5, les fonctions ne sont pas des variables de premier ordre, et beaucoup de fonctionnalités de POO sont médiocres. Javascript est très dynamique et s’appuie sur « l’héritage par prototypes », ce qui est un paradigme significativement différent de l’héritage par classes. De telles différences ne sont pas directement liées aux questions de binding, mais démontrent que la syntaxe et le comportement traditionnel de la programmation objet revêtaient peu d’importance pour les concepteurs du JavaScript.

En JavaScript les références sont toujours explicites, et peuvent facilement se perdre, donc une méthode utilisant this ne référencera pas le bon objet dans toutes les situations, sauf si vous forcez la référence. Au final, le binding n’est pas un concept trop compliqué en JavaScript, mais il est bien trop souvent ignoré ou passé sous silence par les « JavaScripteurs », ce qui peux prêter à confusion.

Le premier pas

Considérons les exemples suivants, qui ne semblent pas prêter à conséquence, et comment leurs comportements peuvent sembler imprévisibles.

var martin = {
  name: 'Martin',
  salue: function(personne) {
	alert("Bonjour " + personne + ", mon nom est " + name);
  }
};

martin.salue(« Marc »);
// => « Bonjour Marc mon nom est  »

Ok, ça c’est bizarre. Ou est le nom? Nous somme coupables d’une supposition de binding : notre méthode se réfère juste à name, ce que JavaScript va chercher dans différentes portées valides de variables, qui se terminent avec les propriétés de l’objet window. Bien sur notre fenêtre à une propriété name, mais vide par défaut donc pas de name pour notre alert.

Essayons :

name = 'Raymond'; // Ou de façon explicite: window.name = 'Raymond';
var martin = {
  name: 'Martin',
  salue: function(personne) {
	alert("Bonjour " + personne + ", mon nom est " + name);
  }
};

martin.salue(« Marc »);
// => « Bonjour Marc mon nom est Raymond »

Bon ça c’est peut être joli, mais inutile. Ce que nous voulons est la propriété name de notre objet, pas de celui de l’objet window ! C’est ici que la référence explicite est importante :

var martin = {
  name: 'Martin',
  salue: function(personne) {
	alert("Bonjour " + personne + ", mon nom est " + this.name);
  }
};

martin.salue(« Marc »);
// => « Bonjour Marc mon nom est Martin »

Notez que notre référence à name est maintenant préfixée avec le mot clé this: ceci est une référence explicite. Et effectivement ça fonctionne!
Vraiment?

var martin = {
  name: 'Martin',
  salue: function(personne) {
	alert("Bonjour " + personne + ", mon nom est " + this.name);
  }
};

var fx = martin.salue;
fx(« Marc »);
// => « Bonjour Marc mon nom est  »
// (ou « Bonjour Marc mon nom est Raymond » en fonction de ou vous l’essayez).

Peut être n’êtes vous pas habitués aux langages qui traitent les fonctions comme des variables de premier ordre, dans ce cas la ligne var fx = martin.salue; peut vous paraitre étrange. Ceci n’appelle pas la méthode salue, mais crée juste une référence, un genre d’alias, si vous préférez. C’est pourquoi appeler fx fini par appeler la méthode salue. Cependant, il semble que nous ayons un problème : nous utilisons explicitement le mot clé this, et pourtant il n’utilise pas Martin. Que se passe t’il?

Ceci est le principal problème avec le binding en javascript, je le nomme « perte de binding ». Cela se produit chaque fois vous accèdez à une méthode grâce à une référence, plutôt que directement avec son objet parent. La méthode perd son binding implicite, et this cesse de référencer l’objet parent pour revenir à sa valeur par défaut, qui dans notre cas est window (donc si window avait une propriété name à ce moment là, elle serait utilisée).

Reconnaitre les structures de code sensibles au binding.

Les structures de code sensibles au binding impliquent des méthodes passées par références, ce qui ce produit principalement de deux façons : soit vous assignez une méthode comme un variable, soit vous passez une méthode en argument (ce qui finalement revient au même si vous y réfléchissez).

Regardons la définition de classe ci-dessous :

function Personne(prenom, nom, age) {
  this.prenom = prenom;
  this.nom = nom;
  this.age = age;
}

Personne.prototype = {
nomComplet: function() {
alert(this.prenom + ‘ ‘ + this.nom);
},
salue: function(autre) {
alert(« Bonjour  » + autre.prenom + « , je suis  » + this.prenom + « . »);
}
};

Essayons pour voir :

var elodie     = new Personne('Elodie', 'Jaubert', 27);
var christophe = new Personne('Christophe', 'Porteneuve', 30);
christophe.salue(elodie);
// => "Bonjour Elodie, je suis Christophe."

Jusqu’ici tout va bien. Continuons :

function plusieursFois(n, fx, arg) {
  for (var index = 0; index < n; ++index) {
	fx(arg);
  }
}

plusieursFois(3, christophe.salue, elodie);
// => 3 fois « Bonjour Elodie, je suis undefined. »
plusieursFois(1, elodie.nomComplet);
// => « undefined undefined »

Oulah, on a un problème! C’est quoi ce undefined? Nous avons perdu notre binding quand nous avons passé salue et nomComplet comme arguments, donc les this pointent vers l’objet window, qui n’a pas de propriétés prenom ou nom. Paf!

Quand vous composez tout votre javascript à la main, comme nous venons de le faire, vous êtes en général plus attentif à ce genre de problèmes. Mais quand vous vous appuyez sur un framework pour assurer la base, le binding peut vous pièger, vous offrant la possibilité d’un code simple, mais qui plante. Regardons le fragement de code basé sur Prototype suivant :

this.elements.each(function(element) {
  // prendre l'element en compte
  this.elementPrisEnCompte(element);
});

Ce code déclenchera une erreur disant que la méthode elementPrisEnCompte est undefined. Pourquoi donc? Parce que vous avez fourni à each une référence à une fonction anonyme, donc this dans celle ci référence l’objet window, pas l’objet extérieur fourni au each. Ceci une erreur très commune, et concerne une bonne partie des questions sur les mailings listes du framework.

Référencer de façon explicite

Donc comment on résout le problème? On fait un bindings explicite, c’est à dire que nous précisons explicitement où pointe this dans la méthode où on l’utilise. Et comment on fait? JavaScript nous offre le choix : apply ou call.

Appliquons nous.

Toute fonction javascript est livrée avec une méthode apply qui vous autorise à l’appeler avec un binding spécifique (un this spécifique, si vous voulez). Cette méthode prend deux arguments : l’objet à utiliser et un tableau d’arguments à fournir à la fonction. Voici un exemple basé sur notre code précédent :

var fx = christophe.salue;
fx.apply(christophe, [elodie]);
// => "Bonjour Elodie, je suis Christophe."

Ce qu’il y a de sympa avec les tableaux, c’est que vous n’avez pas besoin de savoir à l’avance de quels arguments la fonction, sur laquelle vous allez utiliser apply, à besoin. Vous pouvez écrire du code indépendant de la liste d’arguments réelle, construisez simplement le tableau comme bon vous semble, et faites passer. Vous pouvez également prendre un tableau d’arguments existants, le bidouiller jusqu’à plus soif et le passer à la fonction.

Appelons, maintenant!

Si vous savez précisément quels arguments vous aller passer à la fonction, call parait plus convivial, puisque call prends les arguments, pas un tableau d’arguments:

var fx = christophe.salue;
fx.call(christophe, elodie);
// => "Bonjour Elodie, je suis Christophe."

Cependant, avec call, vous perdez la flexibilité du tableau. Cela dépend au final de chaque situation : en dehors de cette différence, apply et call ont une sémantique et un comportement identiques.

Notez, au passage, que la méthode n’appartient pas réellement à l’objet auquel vous faites référence : du moment qu’il utilise this d’un manière compatible avec son binding (comprendre : avec des éléments qui existent dans l’objet lui même), on est tranquille. Un telle flexibilité est possible parce que JavaScript est un langage dynamique, qui recherche les méthodes et variables d’un objet au moment de l’exécution, quand on essaye d’y accéder. Un comportement parfois appelé « binding tardif ». Le binding tardif est présent dans quasiment tous les langages de script (Perl, Ruby, Python, PHP) et, soit dit en passant, OLE Automation.

Brisons les chaines.

C’est cool d’avoir un moyen de spécifier le binding, mais le problème est : on ne peut le spécifier qu’au moment de l’exécution. Vous ne pouvez pas, disons, le spécifier à l’avance et laisser un autre bout de code appeler votre méthode joliment encapsulée quand il le juge nécessaire. Ceci est un gros problème, parce que le but de passer des références, c’est justement ça : laisser un autre code choisir quand appeler une méthode.

Donc ce que nous voulons est une technique pour référencer de façon persistante une méthode, en gros avoir une référence à la méthode encapsulée et livrée avec l’objet. Le seul moyen d’arriver à nos fins est d’encapsuler notre méthode originale dans une autre méthode qui fera l’apply pour nous. Voici l’idée :

function encapsuleur(objet, methode) {
  return function() {
	return methode.apply(objet, arguments);
  };
}

Si vous n’êtes pas trop à l’aise avec le JavaScript, le code ci-dessus peut vous perturber. L’idée est qu’en appellant encapsuleur avec un objet donné et une méthode (qui, à priori, appartient à l’objet) va produire une toute nouvelle fonction (celle anonyme que nous renvoyons)

Cette fonction, lorqu’elle est appellée, va prendre notre méthode originelle et utiliser applydessus, en lui fournissant :

  1. la référence originelle à l’objet (la variable appelée objet), et
  2. les arguments qu’on nous a fourni au moment de l’appel, sous forme de tableau.

(Chaque fonction a une variable prédéfinie arguments qui se comporte comme un tableau de tous les arguments qui lui sont fournis)

Essayons pour voir :

var chrisSalue = encapsuleur(christophe, christophe.salue);
chrisSalue(elodie);
// "Bonjour Elodie, je suis Christophe."

Ah-ha! Ça marche! Nous avons crée une méthode dont les références se limitent à christophe et sa méthode salue.

Les frameworks JavaScript le font aussi

Notre fonction encapsulation est sympa, mais semble un peu limitée. Si vous êtes malin, vous vous appuyez surement sur un framework JavaScript pour éviter les soucis de compatibilité entre navigateurs, faciliter votre gestion DOM, et profiter de quelques fonctionnalités supplémentaires. Faisons le tour de quelques frameworks JavaScripts populaires pour voir comment ils gèrent le binding.

Prototype

Prototype à depuis longtemps équipé les fonctions avec une méthode bind qui vous permet justement de faire ça :

var chrisSalue = christophe.salue.bind(christophe);
chrisSalue(elodie);

Trop de gens savent que bind vous autorise aussi à faire de « l’application partielle », c’est à dire pré-remplir un argument ou plus. Par exemple, admettons que nous ayons une méthode qui active ou désactive une fonctionnalité :

var superFonctionnalite = {
  // ...
  bascule: function(active) {
	this.active = active;
	// ...
  },
  // ...
};

Vous pouvez facilement créer deux raccourcis activer et desactiver de la façon suivante :

superFonctionnalite.activer    = superFonctionnalite.bascule.bind(superFonctionnalite, true);
superFonctionnalite.desactiver = superFonctionnalite.bascule.bind(superFonctionnalite, false);

// et ensuite :
superFonctionnalite.activer();

Une note en passant sur l’utilisation appropriée : parfois, bind était utilisé pour pré-remplir uniquement, sans considération pour le binding. Des choses comme ce qui suis ont déjà étées vue dans du code :

function plusieursFois(count, fx) {
  for (var index = 0; index < count; ++index) {
	fx();
  }
}
// ...
var troisFois = plusieursFois.bind(null, 3);
// ...
troisFois(uneFonction);

Donc, au passage, depuis Prototype 1.6, si la seule chose qui vous intéresse est de pré-remplir, préférez le curry, cela conserve la référence courante et ne s’occupe que de pré-remplir les arguments :

var troisFois = plusieursFois.curry(3);

Ext JS

La bibliothèque Ext JS gère le binding avec une méthode ajoutée aux fonctions, appelée createDelegate. La syntaxe donne ceci :

method.createDelegate(scope[, argArray] [, appendArgs = false])

Notez en premier lieu que les arguments que vous pouvez préciser sont fournis sous forme de tableau, plutôt qu’en liste : maMethode.createDelegate(scope, [arg1, arg2]), pas maMethode.createDelegate(scope, arg1, arg2).

Une autre nuance d’importance est que ces arguments remplaceront les arguments que vous pouvez fournir au moment de l’appel, plutôt que d’être appliqués partiellement. Si vous voulez une application partielle, vous devez passer true (qui est à mettre après le tableau, alors que Prototype le demande avant), ou fournir une position d’insertion en troisième argument (en particulier, en utilisant zéro cela permets de le mettre avant). Voici un exemple tiré de la documentation de l’API :

var fn = scope.func1.createDelegate(scope, »
[arg1, arg2], true);
fn(a, b, c); // => scope.func1(a, b, c, arg1, arg2);

var fn = scope.func1.createDelegate(scope, »
[arg1, arg2]);
fn(a, b, c); // => scope.func1(arg1, arg2);

var fn = scope.func1.createDelegate(scope, »
[arg1, arg2], 1);
fn(a, b, c); // => scope.func1(a, arg1, arg2, b, c);

Dojo

La boite à outils Dojo fourni de quoi faire du binding via la function, nommée de façon humoristique, hitch (NDT:accrocher). La syntaxe est :

dojo.hitch(scope, methodOrMethodName[, arg...])

Ce qui est intéressant, c’est que la méthode peut être passée directement, ou en utilisant son nom. Les arguments complémentaires, si nécessaires, sont à fournir avant les arguments appelés au moment de l’appel. Voici quelques exemples :

var fn = dojo.hitch(scope, func1)
fn(a, b, c); // => scope.func1(a, b, c);

var fn = dojo.hitch(scope, func1, arg1, arg2)
fn(a, b, c); // => scope.func1(arg1, arg2, a, b, c);

Base2

La superbe bilbiothèque Base2 de Dean Edwards est une sorte de plus petit dénominateur commun de toutes les bibliothèques JavaScript, applanissant toutes les petites différences énervantes des implémentations de JavaScript. Elle reconnait qu’un outil de binding est nécessaire et fournit une simple fonction bind :

base2.bind(method, scope[, arg]);

Notez que l’objet concerné (scope) viens en second, pas en premier. Á part ça, la sématique est strictement identique au bind de Prototype ou au hitch de Dojo :

var fn = base2.bind(func1, scope)
fn(a, b, c); // => scope.func1(a, b, c);

var fn = base2.bind(func1, scope, arg1, arg2)
fn(a, b, c); // => scope.func1(arg1, arg2, a, b, c);

jQuery

jQuery ne founit aucun outil de binding. La philosophie de la biliothèque favorise les fermetures au détriment du binding, et force les utilisateurs à sauter de pierre en pierre (C’est à dire à utiliser manuellement les fermetures lexicales et apply ou call, ce que les autres librairies font de toutes façon en interne) quand ils ont réellement besoin de passer des bouts de code référençant des méthodes ou des variables de l’instance d’un objet.

Devrait t’on s’embêter à faire des références ?

Maintenant que nous avons vu tous les détails du binding, il me paraît important de préciser que, parfois, le binding est contre-productif. En particulier, il y a une structure de code dans laquelle on peut se dispenser de binding, avec une amélioration notable de la preformance, en utilisant la fermeture lexicale. (Si vous ne savez pas ce qu’est une fermeture lexciale, ne paniquez pas.)

Voici la structure : une partie du code d’une méthode a besoin d’une fonction anonyme passée par référence pour fonctionner. Cette fonction anonyme a besoin d’accèder au this de la méthode qui l’appelle. Par exemple, admettons un instant que nous avons un itérateur each sur un tableau, reconsiderons le code suivant:

// ...
traiteElements: function() {
  this.elements.each(function(element) {
    // traiter l'objet
    this.elementPrisEnCompte(element);
  });
},
// ...

Le problème ici est que la fonction anonyme qui fait réellement quelque chose est fournie comme argument à each, et donc perd la référence courante. Quand elle essaye d’appeler this.elementPrisEnCompte, elle plante car window n’a pas cette méthode.

Beaucoup de développeurs résolvent le problème un peu trop rapidement avec du binding. En utilisant Prototype, par exemple, ils ajouteraient la bidouille suivante :

// ...
traiteElements: function() {
  this.elements.each(function(element) {
    // traiter l'objet
    this.elementPrisEnCompte(element);
  }.bind(this));
},
// ...

Remarquez l’appel final à bind. Cependant ce code n’est pas une aussi bonne idée que ça en à l’air. En effet nous savons que pour faire une telle référence il faut encapsuler la méthode originelle avec une fonction anonyme. Ce qui signifie qu’appeler la méthode se traduit par deux appels de méthodes : notre encapsulation anonyme et la méthode originelle. Et s’il y a une vérité à presque tous les languages, c’est qu’un appel de méthode est coûteux.

Dans cette situtation, nous avons accès au this originel tant désiré, au même endroit du code ou nous définissons la fonction erronée (la fonction anonyme que nous passons comme argument à each). Nous pouvons simplement sauvegarder le bon this dans une variable locale et l’utiliser dans notre fonction itérative :

// ...
traiteElements: function() {
  var ceci = this;
  this.elements.each(function(element) {
    // Process item
    ceci.elementPrisEnCompte(element);
  });
},
// ...

Regardez, et hop! Pas de binding! Ce code utilise une fonctionnalité du langage appelée « fermeture lexicale ». En gros, la fermeture permet au code à un moment t d’accèder aux variables déclarées dans la porté supérieure. Ici notre fonction anonyme a accès aux variables définies dans la fonction encapsulante, notre méthode traiteElements. La fermeture est conservée lors de l’exécution, quoiqu’il advienne, donc il n’y pas de cout supplémentaire à son utilisation. Et quand bien même il y en aurait, je suis presque sûr que ce serait moins couteux qu’un appel de fonction à chque itération.

Soyez prudent avec vos bindings: parfois la fermeture offre un solution plus efficace, plus simple et plus courte. (Ce qui est, je pense, la raison qui à poussé jQuery à obliger ces utilisateurs à réfléchir à la meilleurs option pour chaque cas, en les laissant se préocupper du binding manuellement.) Bien que les fermetures génèrent leurs propres problèmes, elle peuvent entrainer des fuites mémoire dans certains navigateurs, l’utilisation que je préconise ici est relativement sûre.

Avant de partir…

Pour résumer :

  • Un accès au membre (méthode ou propriété) d’un objet doit être qualifié avec l’objet auquel il appartient, même si c’est this.
  • Toute référence à une fonction (assignation comme valeur, passage comme argument) perd le binding original de la fonction.
  • JavaScript fournit 2 moyens équivalents de spécifier explicitement le binding d’une fonction quand on l’appelle : apply et call.
  • Créer une référence à une méthode liée à son objet contenant requiert une fonction d’encapsulage anonyme, et a un coût d’appel. Dans certaines situations, utiliser les fermetures peut être une meilleure solution.

Et maintenant, avec l’aide de cet article, vous n’aurez plus de problèmes de liaisons en JavaScript!

A propos de l’auteur

Christophe conçoit et implémente des « web apps » depuis 1995. Après avoir créé le premier portail JSP en Europe, il a dirigé le departement de génie logiciel d’un grande école d’ingénierie informatique, puis a transmi ses techniques de dévelloppement web de pointe et son amour de Rails à Ciblo.net, ou il est actuellement directeur technique.

ISSN: 1534-0295 Copyright © 1998-2009
A List Apart Magazine and the authors.

Translated with the permission of A List Apart Magazine and the author[s].

Traduit avec l’authorisation d’A List Apart Magazine et de l’auteur.

Article original : Getting Out of Binding Situations in JavaScript