Chaque développeur javascript a un jour ou l’autre rencontré un problème avec le contexte associé à this
.
Prenons l’exemple de code simpliste suivant :
var myObject = { crazyMessage: 'gouzigouza', doSomethingCrazy: function() { alert(this.crazyMessage); } }; myObject.doSomethingCrazy();
Mon objet contient une propriété crazyMessage
et une méthode doSomethingCrazy
qui utilise la propriété crazyMessage
.
Jusqu’ici tout va bien, le contexte de this
au sein de la méthode doSomethingCrazy
est bien l’objet myObject
.
Maintenant, dans un élan de folie, je veux implémenter la méthode doSomeAsyncCrazyness
:
var myObject = { crazyMessage: 'gouzigouza', doSomethingCrazy: function() { alert(this.crazyMessage); }, doSomeAsyncCrazyness: function() { setTimeout(function() { this.doSomethingCrazy(); }, 1000); } }; myObject.doSomeAsyncCrazyness();
Aussi surprenant que cela puisse paraître, je récupère une erreur dans ma console :
Uncaught TypeError: undefined is not a function
Cette erreur est due à this
qui est désormais associé au contexte global et non plus à mon objet myObject
, à cause de l’appel à setTimeout
.
Mettre this
en « cache »
Pour pallier ce problème, l’approche très souvent adoptée par le développeur est d’assigner this
à une variable intermédiaire temporaire de type that
ou self
de la manière suivante :
var myObject = { crazyMessage: 'gouzigouza', doSomethingCrazy: function() { alert(this.crazyMessage); }, doSomeAsyncCrazyness: function() { var that = this; setTimeout(function() { that.doSomethingCrazy(); }, 1000); } }; myObject.doSomeAsyncCrazyness();
De cette manière, l’appel à la méthode doSomeAsyncCrazyness
fonctionne en effet comme prévu.
Une méthode bien plus élégante existe cependant et ce depuis ECMAScript 5.
.bind() magic
La méthode .bind()
disponible sur le prototype de Function
résoud notre problème en permettant, entre autres choses, de définir explicitement le contexte associé à this
.
Nous pouvons donc réécrire notre code précédent de la manière suivante :
var myObject = { crazyMessage: 'gouzigouza', doSomethingCrazy: function() { alert(this.crazyMessage); }, doSomeAsyncCrazyness: function() { setTimeout(function() { this.doSomethingCrazy(); }.bind(this), 1000); } }; myObject.doSomeAsyncCrazyness();
Avouez que c’est quand même plus joli.
Support des vieux navigateurs
Et si mon code doit s’exécuter dans un navigateur qui ne supporte pas ES5 ? Internet Explorer < 9 pour ne pas le nommer.
Dans ce cas, plusieurs solutions.
- Continuer avec la technique de mise en cache de
this
- Utiliser la méthode
.bind
implémentée dans la plupart des frameworks JS (Angular.bind()
par exemple) - Implémenter un fallback quand la méthode n’est pas disponible
Mozilla notamment propose le fallback suivant (Mozilla polyfill Function.prototype.bind()) :
if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { if (typeof this !== "function") { // closest thing possible to the ECMAScript 5 // internal IsCallable function throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, fNOP = function () {}, fBound = function () { return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }; }
Pour aller plus loin
Quelques ressources pour aller plus loin :
Merci pour cette petite astuce !