Archives par étiquette : es5

Utilisez la méthode Function.prototype.bind() de Javascript

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 :