Archives de catégorie : Javascript

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 :

Transpiler son code ES6 avec Gulp et Traceur

L’actualité est de plus en plus riche pour les développeurs web ces derniers temps. Entre Hack, HVVM, Symfony 3, Angular 2.0, Ember 1.6 ou ECMAScript 6, on ne sait plus où donner de la tête.

Alors que dans certaines entreprises, on se bat encore pour abandonner les navigateurs types IE6/7/8 et enfin pouvoir utiliser ES5 sans problème, ailleurs, on commence déjà à développer en ES6. C’est notamment le cas de la team AngularJs qui a annoncé que Angular 2.0 serait écrit en ES6. La démo du travail en cours sur le nouveau framework de DI d’Angular a même été faite avec des bouts de code en ECMAScript6.

Et la bonne nouvelle, c’est que si les navigateurs modernes sont encore loin de supporter le futur ES6 (beaucoup de nouvelles features restent à l’état de draft), rien n’empêche de le tester dès aujourd’hui. Et pourquoi pas même, en production… Madness?! Un peu je dois bien l’avouer… Mais si vous avez le goût du risque, après tout, pourquoi pas… J’ai bien eu l’occasion de travailler sur une grosse application avec Ember-Data en v0.13…

Quelles solutions pour tester ES6 ?

Alors comment faire pour tester ES6 ? Plusieurs solutions s’offrent à nous. Une des plus simples si on souhaite uniquement jouer avec les nouveaux modules ou s’essayer au modèle objet, c’est d’utiliser un équivalent à jsfiddle, compatible ECMA6. C’est le cas par exemple de es6fiddle.

Utiliser Gulp et Traceur pour « traduire » son code ES6 vers ES5

En revanche, si l’on souhaite commencer à coder une appli réelle en ES6 et la voir s’exécuter dans un navigateur moderne, il faut se tourner vers des solutions telles que le projet Traceur de Google.

Traceur is a JavaScript.next-to-JavaScript-of-today compiler that allows you to use features from the future today.

Traceur est ce qu’on appelle un transpiler, c’est à dire, un compilateur qui compile le code source d’un langage vers le code source d’un autre. En l’occurence donc, un transpiler ECMAScript6 vers ECMAScript5.

Traceur supporte une liste importante des features populaires d’ES6 et, cerise sur le gâteau, il a son propre plugin Gulp.

Commençons donc par se créer un répertoire pour notre projet et y installer gulp et son plugin gulp-traceur :

mkdir projet-es6 && cd projet-es6
npm init
npm install --save-dev gulp gulp-traceur
mkdir dist src

npm init

On crée ensuite un gulpfile simpliste :

// gulpfile.js
var gulp = require('gulp'),
    traceur = require('gulp-traceur')
    paths = {
       scripts: ['src/*.js', 'src/**/*.js']
    };

gulp.task('scripts', function () {
    gulp.src(paths.scripts)
        .pipe(traceur())
        .pipe(gulp.dest('dist'));
});

gulp.task('watch', function() {
  gulp.watch(paths.scripts, ['scripts']);
});

gulp.task('default', ['scripts', 'watch']);

Et voilà, nous sommes prêts à tester ES6 et à le transpiler vers ES5 grâce à Gulp.

On peut essayer avec une Arrow Function par exemple :

// src/app.js
var square = x => x * x;

Après avoir lancé la commande gulp scripts, notre fonction arrow est transformée en une bonne « vieille » fonction ES5 :

"use strict";
var __moduleName = "app";
var square = (function(x) {
  return x * x;
});

Ci-dessous, le résultat de transpiling avec une classe ES6 toute simple :

Screencast Gulp Traceur ES6

Il ne nous reste plus qu’à jouer avec les autres features spécifiques ES6 :

  • Let et Const
  • Paramètres par défaut
  • Proxies
  • Modules
  • Destructuring

Pour aller plus loin

On trouve toute une flopée d’exemples et de resources à propos d’ECMAScript 6. Je trouve ce document assez bien fait : https://github.com/lukehoban/es6features pour se lancer.

Quelques autres resources intéressantes :

Designez des API asynchrones VRAIMENT asynchrones

Je suis tombé récemment sur un vieil article d’Isaac Zimmitti Schlueter à lire ABSOLUMENT.

Pour résumer rapidement le propos, IZS explique suite à un avertissement on ne peut plus clair (Do Not Release Zalgo) que lors du design d’une API Asynchrone, il faut éviter à tout prix la situation suivante :

var cachedData;
function getSomeData(callback) {
  if (cachedData === undefined) {
    $.get('some/random/api').then(function(data) {
      callback(cachedData = data);
    });
  } else {
    callback(cachedData);
  }
}

Quel est le problème avec ce bout de code ?

Si vous êtes développeur AngularJs / EmberJs / votre framework javascript préféré, je suis certain que vous avez vous-même été amené à écrire du code similaire ou au moins à en rencontrer au sein de vos applications.

Alors quel est le problème ?

Mon API fait un calcul coûteux (requête HTTP vers une API tiers) au premier appel, stocke la donnée retournée dans une variable servant de cache et appelle le callback. Au deuxième appel, mon API constate que la donnée est déjà en cache et appelle immédiatement le callback.

L’utilisation d’une API de ce type est extrêmement complexe et source de bugs. Pourquoi ? Son comportement est difficile à prédire…

L’auteur l’explique en ces termes sur son article :

If you have an API which takes a callback, and sometimes that callback is called immediately, and other times that callback is called at some point in the future, then you will render any code using this API impossible to reason about, and cause the release of Zalgo.

Prenons l’exemple du code javascript suivant :

// Quelque part dans mon code
getSomeData(function(result) {
  // Faire quelque chose avec mon résultat
});

function clickHandler() {
  var data;
  getSomeData(function(result) {
    data = result;
  });

  if (data.id === 1) {
    // Traitement important
    alert('gouzigouza');
  }
}

Ligne 2, on appelle notre API asynchrone. Un peu plus bas, on définit une fonction appelée lors d’un clic sur un bouton dans une page web. Au sein de ce clickHandler on utilise l’objet data en partant du principe qu’il est disponible immédiatement. L’erreur saute ici évidemment aux yeux. Rien ne garantit que notre objet data ait déjà été retourné arrivé à la condition if.

Malheureusement, le design de notre API et l’utilisation qui en a été faite ici fait que le bug n’est pas identifiable immédiatement. En revanche, si le développeur supprime le premier appel à notre API ligne 2 ou si le clic sur le bouton au sein de la page web intervient avant que le résultat de notre API asynchrone n’ait été mis en cache, alors on se retrouvera avec une belle erreur javascript :

TypeError: Cannot read property 'id' of undefined

Quelle solution ?

Pour une API asynchrone, l’objectif est donc de toujours respecter le caractère asynchrone.
Pour se faire, une approche simpliste est de modifier notre API de la manière suivante :

var cachedData;
function getSomeData(callback) {
  if (cachedData === undefined) {
    $.get('some/random/api').then(function(data) {
      callback(cachedData = data);
    });
  } else {
    setTimeout(function() {
      callback(cachedData = data);
    }, 0);
  }
}

Ici, l’utilisation du setTimeout() est une astuce qui permet de forcer l’appel du callback de manière asynchrone. Si l’API était codée avec nodejs, il serait bien plus judicieux d’utiliser process.nextTick() qui permet de mettre en place un comportement similaire.

Ainsi, à l’usage, il est désormais systématiquement impossible de faire fonctionner le code suivant :

var data;
getSomeData(function(result) {
  data = result;
});

if (data.id === 1) {
  // Bug systématique
  alert('gouzigouza');
}

Le développeur identifie immédiatement son erreur et est obligé alors de modifier son code de la manière suivante :

getSomeData(function(result) {
  var data = result;
  if (data.id === 1) {
    alert('gouzigouza'); // OK
  }
});

Pour aller plus loin

Voici quelques liens pour approfondir le sujet. Je vous conseille évidemment la lecture de l’article de IZS qui ajoute des exemples de design pour des API où les données sont généralement présentes immédiatement et où les performances sont très importantes.
Vous pouvez également aller jeter un oeil à la partie Keeping callbacks truly asynchronous de cet article sur la fonction process.nextTick de nodejs.

AngularJs : ngIf ngSwitch VS ngShow ngHide

Depuis la sortie de Angular 1.1.5 (Triangle Squarification), la directive ngIf a été ajoutée au framework AngularJs pour permettre de créer ou supprimer un bout de DOM en fonction de l’évaluation d’une expression booléenne, comportement auparavant possible uniquement grâce à la directive ngSwitch.

La directive ngIf s’utilise de manière similaire à la directive ngShow. Voici deux exemples qui produisent visiblement le même résultat.

ngIf

<!-- Exemple avec ngIf -->
<label>Afficher / masquer : <input type="checkbox" ng-model="show" /></label>
<span ng-if="show">Ceci est un test</span>

ngShow

<!-- Exemple avec ngShow -->
<label>Afficher / masquer : <input type="checkbox" ng-model="show" /></label>
<span ng-show="show">Ceci est un test</span>

Quelle différence ?

Dans les faits, ces deux directives ne se comportent absolument pas de la même manière. ngShow se contente d’afficher ou masquer un élément du DOM en lui ajoutant ou supprimant la classe CSS ng-hide définie par défaut de la manière suivante :

.ng-hide {
  display:none !important;
}

L’élément en question reste bien systématiquement présent dans le DOM, seule sa visibilité varie grâce à cette classe CSS.

Concernant la directive Angular ngIf, cette dernière procède à une suppression ou un ajout dans le DOM de l’élément concerné, en fonction de l’évaluation de l’expression booléenne associée.

ngIf differs from ngShow and ngHide in that ngIf completely removes and recreates the element in the DOM rather than changing its visibility via the display css property.

La première différence qui saute aux yeux est l’impact au niveau des sélecteurs de type pseudo-classe (pseudo-class selector) que ce soit pour le CSS ou via une directive faisant usage de jQuery ou jqlite.

A common case when this difference is significant is when using css selectors that rely on an element’s position within the DOM, such as the :first-child or :last-child pseudo-classes

.menu li {
  display: inline-block;
  margin: 10px 0;
  padding: 5px;
}

.menu li:first-child {
  margin-left: 30px;
}

.menu li:last-child {
  margin-right: 30px;
}
<label>Je suis un administrateur : <input type="checkbox" ng-model="user.isAdmin" /></label>
<ul class="menu">
  <li>Menu 1</li>
  <li>Contact</li>
  <li ng-show="user.isAdmin">Menu Admin</li>
</ul>

Dans l’exemple ci-dessus, on ajoute une marge spécifique au premier et au dernier élément du menu. Mais avec l’utilisation de la directive ngShow on n’obtient pas le rendu initialement souhaité par notre intégrateur. Le menu spécifique à l’administrateur, bien que non visible pour un utilisateur lambda, reste bien présent dans le DOM et l’entrée contact n’est dès lors pas ciblée par notre pseudo-classe :last-child.

Impact sur les performances

Dans la majorité des cas, l’utilisation de ngIf par rapport à ngShow n’a pas d’impact perceptible sur les performances. Il est pourtant légitime de se poser la question lorsqu’on travaille au sein d’une liste importante en terme de volumétrie, typiquement au sein d’un ngRepeat sur plusieurs centaines voire milliers d’éléments.

Prenons l’exemple suivant. Nous avons une liste importante (> 1000 éléments) et nous avons deux types d’informations à afficher ; le nom de l’élément et potentiellement sa description.

<ul>
  <li ng-repeat="element in elements">
    <span>{{ element.name }}</span>

    <!-- Avec la directive ngShow -->
    <span ng-show="element.showDescription">{{ element.description }}</span>

    <!-- Avec la directive ngIf -->
    <span ng-if="element.showDescription">{{ element.description }}</span>
  </li>
</ul>

Deux cas très différents se profilent. Soit la condition d’affichage / masquage est susceptible de varier régulièrement en fonction des interactions de l’utilisateur, soit elle varie rarement.
Par exemple, si l’affichage / masquage de la description est déclenché suite au cochage / décochage d’une option en haut de page, il semble plus avantageux d’utiliser la directive ngShow. L’élément est déjà inséré dans le DOM et il ne reste plus qu’au navigateur à recalculer sa visibilité suite à l’ajout ou la suppression de la classe ng-hide. D’autre part l’utilisation de ngIf a pour effet de créer un scope spécifique à l’élément ciblé ce qui entraîne nécessairement sa destruction lorsque la condition booléenne est évaluée à false, et donc un temps de traitement associé.

En revanche, si la condition d’affichage de la description dépend par exemple des droits associés à l’utilisateur connecté, mieux vaut utiliser la directive ngIf. Ainsi, si l’utilisateur n’a pas le droit de voir la description, le DOM est très largement allégé (et donc l’emprunte mémoire associée). C’est d’autant plus vrai que l’élément ciblé par la directive ngIf/ngShow/ngHide est important.

Attention toutefois, il peut être bien plus avantageux de travailler à limiter le nombre de $watch générés au sein d’un ng-repeat en codant sa propre directive qui ne crée aucun watchers ou en faisant usage de directives optimisées telles que la désormais fameuse BindOnce.

ngIf et scope enfant (child scope)

Je l’ai évoqué un peu plus haut, contrairement à ngShow ou ngHide, ngIf crée un scope enfant pour l’élément ciblé.

Note that when an element is removed using ngIf its scope is destroyed and a new scope is created when the element is restored. The scope created within ngIf inherits from its parent scope using prototypal inheritance.

Ainsi, sur le bout de code qui suit, le binding au model ne se comporte pas de la même manière alors que la seule différence d’implémentation réside au niveau de l’utilisation d’un ngShow VS ngIf.

function MyCtrl($scope) {
    $scope.show = true;
    $scope.value = "gouzigouza";
}
<div ng-app>
    <div ng-controller="MyCtrl">
        <label>Afficher / masquer</label> <input type="checkbox" ng-model="show" />
        <div>
            <label>Scope parent</label> <input type="text" ng-model="value" />
        </div>
        <div ng-show="show">
            <label>ngShow</label> <input type="text" ng-model="value" />
        </div>
        <div ng-if="show">
            <label>ngIf</label> <input type="text" ng-model="value" />
        </div>
    </div>
</div>

Vous pouvez voir le résultat sur le fiddle ci-dessous. Essayez de modifier la valeur des différents inputs. Vous constaterez que le troisième input est bindé sur la même valeur que les 2 premiers, tant que sa propre valeur n’a pas été modifiée via le champ input lui-même. Vous pouvez également constater que si la valeur de ce dernier est modifiée et que la case à cocher permettant d’activer ou non la condition du ngIf varie, la valeur de l’input est ré-initialisée à celle du parent scope.

Conclusion

Même si les directives ngShow, ngHide et ngIf semblent produire le même résultat, leur implémentation et leur comportement diffèrent. Il est important de bien comprendre leur fonctionnement pour les utiliser à bon escient.

Pour aller plus loin