Comprendre le sucre syntaxique d'ES6 vers JS

Tandis que le support d'ECMAScript6 est de plus en plus répandu, il est encore beaucoup trop tôt pour s'attendre à un support de tous les navigateurs sur un site grand public. Un transpileur est donc obligatoire pour utiliser le Javascript de demain, mais dès aujourd'hui !

Voici un petit tour d'explication des sucres syntaxiques d'ES6 avec leur version transpilée, telle que générée par Babel.

Templates literals (AKA templates de string)

On va commencer cet article doucement avec les templates de strings.

En JS, il n'est pas rare de concaténer plusieurs strings entre elles pour en générer une nouvelle. Par exemple a+b+', '+c+', '+d. Non seulement cette syntaxe est peu claire lorsque l'on doit concaténer beaucoup d'éléments, mais en plus l'opérateur + a un fonctionnement différent suivant le type des paramètres. Dans cet exemple, si a et b sont deux strings, on aura une concaténation, mais s'ils sont des entiers, on se retrouve avec une addition !

ES6 introduit donc les templates. Globalement on va définir le format de notre string avec des backquotes. On va ensuite indiquer avec ${} les parties a remplacer. On se retrouve donc avec le code suivant :

//ES6
a = 1
b = 2
c = "test"
d = "it!"
`${a}${b}, ${c}, ${d}`

//JS
"use strict";
function _templateObject() {
  var data = _taggedTemplateLiteral(["", "", ", ", ", ", ""]);
  _templateObject = function _templateObject() { return data; };
  return data;
}
function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
a = 1;
b = 2;
c = "test";
d = "it!"(_templateObject(), a, b, c, d);

Le code JS, est quasiment illisible.

Pour expliquer simplement l'algorithme, babel génère une fonction qui, à partir de ses arguments, va créer un tableau des différentes parties à inclure et joindre les éléments de ce tableau ensemble pour créer le résultat.

Classes

La notion de classe n'existe pas en JS car le langage est orienté prototype. C'est-à-dire que chaque objet est une fonction (le prototype) qui sert à générer le dit objet (les instances) lors de sa génération. Il incombe donc à cette fonction de créer à la fois les variables d'instances, mais aussi les méthodes propres à cet objet.

Sur le papier ce paradigme semble intéressant, mais dans la pratique il est très (trop) proche de l'orienté objet. Beaucoup de programmeurs manquent certaines nuances et finissent par générer des bugs ou du code inéfficient. Par exemple, il est très facile de recopier une fonction à chaque instance et donc de faire exploser la mémoire. Il est facile aussi de se tromper sur l'usage de this et de perdre totalement le fil de son code.

Lors de la transpilation, Babel va se charger, à partir de la syntaxe claire spécifiée par le mot-clé class, de gérer tous ces cas spéciaux pour nous.

Ainsi, on observe la transpilation suivante :

//ES6
class Test {
  
}

//JS
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Test = function Test() {
  _classCallCheck(this, Test);
};

Il faut avouer que la nouvelle syntaxe, en plus de nous garantir le fonctionnement, nous offre une syntaxe ultra claire. Autre exemple encore plus parlant du gain en clareté :

//ES6
class Test {
}

class Test2 extends Test {
}

Là où en ES5, il aurait fallu copier chaque méthode/attribut de la super-classe à la main et pour créer soi-même une simili hiérarchie, l'ES6 nous offre encore une fois une syntaxe concise et toutes les features d'un vrai langage objet.

J'ai évité de mettre le code ES5 généré par Babel car, comme c'était prévisible, il est totalement illisible. Si cela vous intéresse, je vous conseille de faire un tour sur ce transpileur es6 vers es5 en ligne.

Arrow functions

Suivant les langages, ces fonctions s'appellent fonction anonyme, ou lambda (Java) ou encore closure (PHP). C'est un type spécial de fonction qui ne porte pas de nom dans le scope et doit donc être utilisée comme valeur, c'est à dire stockée dans une variable ou utilisée en inline.suivante

La syntaxe est la suivante :

// ES6
var x = x => x + 2;

// JS
var x = function x(_x) {
  return _x + 2;
};

Globalement, ici le transpileur ne fait qu'un changement de syntaxe pour remplacer le sucre en une fonction ES5 standard.

Là où ce sucre prend tout son sens, c'est lorsqu'une fonction anonyme sert de générateur pour une autre fonction :

// ES6
var x = x => x => x + 2;

// JS
var x = function x(_x) {
  return function (x) {
    return x + 2;
  };
};

En revanche il existe des cas spéciaux, notamment lorsque l'arrow function utilise this. Dans la spec ES5, le this référence le lieu d'appel de la fonction. Cela rend le débuggage difficile car, en lisant la fonction, this dépend du lieu d'appel et non pas du lieu de définition. Dans la spec ES6 en revanche, le this doit référencer le lieu de déclaration, ce qui donne la transpilation non-intuitive suivante :

// ES6
var f = function() {
  this.value = 2;
  return () => this.value;
};

// JS
var f = function x() {
  // Garde une réference a this
  var _this = this;
  this.value = 2;return function () {
    // Utilise le this déclaré précédemment
    return _this.value;
  };
};

Dans cet exemple, le transpileur a conservé une référence au lieu de définition de this et nous garantit un fonctionnement identique quel que soit le lieu d'appel de la fonction.

C'est ainsi qu'une classe ES6 peut déclarer ses callbacks comme ça :

//ES6
class Test {
  maFunc = () => console.log(this) //This référencera toujours l'instance de Test qui a créé la fonction maFunc appelée
}

//JS
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var Test = function Test() {
  var _this = this; //This référencera toujours l'instance de Test qui a créé la fonction maFunc appelée
  _classCallCheck(this, Test);
  _defineProperty(this, "maFunc", function () {
    return console.log(_this);
  });
} 
;

En ES5, on aurait dû utiliser .bind(this) sur les fonctions à l'objet dans le constructeur pour obtenir une fonction liée a la bonne instance !

Propriétés d'objet

Ici, il s'agit d'un simple sucre syntaxique qui va simplifier l'assignation d'attributs dans un objet.

Par exemple le code suivant, valide à la fois en ES5 et 6: {a: a, b: b} peux être simplifié en {a, b}. Babel ira tout seul chercher la variable du now de l'attribut dans le scope actuel.

//ES6
a="test"
b="it!"
c = { a, b }

//JS
"use strict";
a = "test";
b = "it!";
c = {
  a: a,
  b: b
};

Cette syntaxe fonctionne aussi pour les déstructurations d'objet :

//ES6
x = 1
y = 2
point = {x, y}
distance=({x, y}) => {
  return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
}

//JS
"use strict";
x = 1;
y = 2;
point = {
  x: x,
  y: y
};
distance = function distance(_ref) {
  var x = _ref.x,
      y = _ref.y;
  return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
};

Qu'avez vous pensé de cet article?