Modernisez votre code Node.js avec EcmaScript 2015

GNU/Linux Magazine HS n° 085 | juillet 2016 | Michael Bailly.
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
6 ans que l'on attendait cela ! La nouvelle version de JavaScript est sortie en août 2015, et elle est maintenant (quasi-)complètement supportée dans Node.js 6. Voici un focus sur les principales nouveautés.

Le langage JavaScript est vieux comme le Web, mais son évolution a été, soyons honnêtes, plutôt lente ces dernières années. Eh bien, attachez votre ceinture, on reprend de la vitesse ! La nouvelle version de JavaScript est sortie en août 2015, et, moins d'un an plus tard, Node.js, avec la version 6, supporte 96% de la norme. De nombreuses nouveautés sont dès aujourd'hui disponibles, ce qui augmente considérablement le terrain de jeu des développeurs, pour leur plus grande joie.

1. Un peu d'histoire

L'histoire du projet Node.js est intéressante. Longtemps sous la coupe d'une société privée, Joyent, le projet avançait lentement. Un fork communautaire non hostile, nommé io.js, a été initié par la communauté afin de montrer à Joyent ce que peut apporter, en terme de dynamisme, de contributions, et donc d'utilisateurs, un pilotage vraiment communautaire. Io.js a finalement été mergé avec node.js, la fondation Node a été créée, tout finit pour le mieux, si ce n'est peut-être pour Joyent qui a perdu la mainmise sur le projet.
Au niveau technique donc, l'évolution quasiment nulle des versions 0.10 à 0.12 a ensuite accéléré.

Le cœur de Node.js est V8, le moteur JavaScript de Google, qui propulse aussi le navigateur Chrome. Pendant longtemps, la version de V8 embarquée dans Node.js avait beaucoup de versions de retard sur le projet parent. Mais, depuis la reprise en main du projet par la communauté, Node.js intègre rapidement les nouvelles versions que sortent les développeurs de V8.

Parallèlement, l'association de standardisation du langage JavaScript, nommé Ecma International (http://www.ecma-international.org/), a arrêté en août 2015 la version 6 de EcmaScript, renommée ensuite EcmaScript 2015. Oui, c'est sûr, c'est un peu dur à suivre. Résumons : EcmaScript = JavaScript, et EcmaScript 2015 est la dernière version du langage. Et que de nouveautés dans cette version ! Le langage de programmation du Web n'avait pratiquement pas évolué depuis sa version 5, en 2009... bonjour le choc ! La dernière version de JavaScript est totalement compatible ascendante : un programme JavaScript écrit en EcmaScript 5 est valide, et fonctionnera exactement de la même manière, lorsqu'il sera exécuté avec un moteur EcmaScript 2015. Don't break the Web, c'est le leitmotiv qui a guidé l'ensemble des choix pour cette nouvelle version.

Cet article a pour sujet de faire découvrir les nouveautés du langage. En effet, Node.js 6, sorti en avril 2016, affiche un support de la norme EcmaScript 2015 à 96% (source http://node.green/). L'objectif n'est pas de détailler toutes les nouveautés, mais plutôt d'en mettre en avant quelques-unes.

2. On commence doucement

Abordons ces nouveautés de JavaScript par des ajouts qui, bien qu'importants, ne peuvent pas être non plus qualifiés de révolution.

2.1 Constructions de langage

2.1.1 Visibilité de variable au niveau bloc

On le sait tous, la visibilité d'une variable JavaScript, déclarée par le mot-clé var, est au niveau de la fonction. La nouvelle instruction let permet de définir une variable définie au niveau bloc.

function example() {

  

  if ( true ) {

    var test = "hello";

  }

  console.log(test); // fonctionne : test est "hoisté" dans le scope de la

                     // fonction example, sa visibilité n'est pas restreinte

                     // au bloc if

 }

Dans l'exemple ci-dessus, la variable test, définie dans un bloc if, est ensuite visible à l'extérieur de ce bloc.

function example() {

  

  if ( true ) {

    let test = "hello";

  }

  console.log(test); // ne fonctionne pas : déclenche une exception "test is undefined"

                     // à la compilation

}

Dans ce deuxième exemple en revanche, la variable test, déclarée avec le mot-clé let, n'est pas visible en dehors du bloc if.

2.1.2 Constantes

Le nouveau mot-clé const permet de déclarer une constante. Attention toutefois, un objet déclaré avec const n'est pas immutable ; on ne peut pas assigner une autre valeur à la constante.

const test = "hello";

test = "bonjour"; // déclenche une exception "TypeError"

const obj = {};

obj.test = "hello";

obj = "another thing"; // déclenche une exception "TypeError"

2.1.3 Destructuring assignment

Derrière ces mots étranges se cachent des raccourcis d'écriture pour assigner et réassigner des variables.

La première forme, relativement incompréhensible à mon avis, permet d'assigner à des variables locales les valeurs de propriétés d'un objet.

const opts = {test: '1', encore: '2'};

let {test: localTest, encore: localEncore} = opts; // on assigne a localTest la valeur de opts.test,

                                                   // et à localEncore la valeur de opts.encore

console.log(localTest, localEncore); // affiche 1, 2

Encore plus concis, si le nom de la variable locale est le même que le nom de la propriété, alors on peut l'omettre :

const opts = {test: '1', encore: '2'};

let {test, encore} = opts; // on assigne a test la valeur de opts.test,

                           // et à encore la valeur de opts.encore

console.log(test, encore); // affiche 1, 2

Rassurez-vous, cela marche aussi sur des tableaux, auquel cas le langage assigne en respectant l'ordre des valeurs contenues dans le tableau :

const opts = ['1', '2'];

let [test, encore] = opts; // on assigne test à la valeur du premier élément du tableau,

                           // et encore à la valeur du deuxième élément

console.log(test, encore); // affiche 1, 2

2.1.4 Fonctions

Il est maintenant possible de spécifier des valeurs par défaut pour les arguments d'une fonction… hourra !

function show(text, timeout = 2000, callback = function() {}) {

  console.log(timeout);

}

show('hello', 2000); // affiche 2000

show('hello'); // affiche 4000

Il est aussi possible de stocker dans un tableau les arguments restants d'une fonction. Cela donne la possibilité de gérer simplement des fonctions dont le nombre de paramètres peut être variable.

function concat(start, ...others) {

  let response = start;

  others.forEach(function(s) { response += s; });

  console.log(response);

}

concat('hello', ' ', 'World'); // affiche "hello World"

concat('hello', ' ', 'my', ' ', 'dear'); // affiche "hello my dear"

Un peu comme les destructured assignments, on peut utiliser les destructured parameters. Ceci est bien pratique pour récupérer les paramètres optionnels.

function show(text,  {timeout, callback} = {} ) {

  setTimeout(function() {

    console.log(text);

    if (callback) { callback(); }

  }, timeout || 0);

}

show('hello', {timeout: 2000});

2.1.5 Objets littéraux

L'utilisation des objets littéraux est tellement fréquente en JavaScript, que cette nouvelle version apporte beaucoup d'amour à cette fonctionnalité majeure.

On dispose maintenant d'un raccourci d'initialisation de propriété, qui est en quelque sorte le contraire de l'object destructuring :

function createPerson(name, age) {

    return {

        name,

        age

    };

    // équivalent à

    return {

      name: name,

      age: age

    };

}

On a la capacité de déclarer de manière plus concise les méthodes d'un objet :

var person = {

    name: "Sacha",

    sayName() {

        console.log(this.name);

    }

    // equivalent à

    sayName: function() {

        console.log(this.name);

    }

};

Il est aussi possible d'assigner des propriétés dont le nom est calculé :

var propertyName = "name";

var person = {

    [propertyName]: "Sacha",

    sayName() {

        console.log(this.name);

    }

};

La méthode statique assign permet de composer des objets.

var obj1 = {name: "Sacha"};

var obj2 = {type: "person"};

Object.assign(obj1, obj2);

// obj1 est maintenant {name: "Sacha", type: "person"}

Cette méthode statique a déjà été implémentée maintes fois auparavant par les bibliothèques, par exemple jQuery (jQuery.extend), underscore (_.extend), ou encore Angular (angular.merge).

La méthode statique setPrototypeOf permet de changer le prototype d'un objet déjà instancié.

let personne = {

    getGreeting() {

        return "Hello";

    }

};

let chien = {

    getGreeting() {

        return "Waf!";

    }

};

let ami = Object.create(personne);

// le prototype de ami est personne

Object.setPrototypeOf(ami, chien);

// le prototype de ami est maintenant chien

On le voit, beaucoup de sucre syntaxique, pour rendre le code plus agréable à écrire et à lire, et quelques fonctionnalités clés sont apparues. Maintenant qu'on est bien chaud, passons aux choses sérieuses.

3. On envoie du lourd

3.1 Promises

De plus en plus de librairies utilisent les systèmes dits de promises, en lieu et place des callbacks. Ceci permet, outre d'éviter le callback hell (aussi appelé pyramid of doom), de bénéficier de morceaux de code unitaires et composables, ainsi que de gérer simplement les erreurs. Je ne détaillerai pas ici le fonctionnement des promises, sachez que jusqu'alors les librairies Q (http://documentup.com/kriskowal/q/) et bluebird (http://bluebirdjs.com/docs/getting-started.html) étaient les références de l'implémentation des promesses dans Node.js, ceci en respectant la norme Promises/A+ (https://promisesaplus.com/). Et bien, maintenant mesdames et messieurs, les promesses sont implémentées directement dans le moteur JavaScript.

Voici un exemple basique ; nous utilisons setTimeout pour émuler un traitement asynchrone :

function asyncStuff() {

  return new Promise(function(resolve, reject) {

    setTimeout(function() {

      resolve('response !');

    }, 1000);

  });

}

asyncStuff.then(function(response) {

  console.log('here is the response', response);

}, function(error) {

  console.log('something bad happened', error);

});

3.2 Arrow functions

Ah… je pense que de toutes les nouveautés, celle-ci est ma préférée ! À la base, on peut voir les arrow functions (fonctions flèche ?) comme un raccourci d'écriture pour déclarer une fonction JavaScript.

Par exemple, le code suivant :

var add = function (first, second) {

return first + second;

}

peut maintenant s'écrire :

var add = (first, second) => first + second;

Notez que, avec cette écriture sans accolades, la fonction retourne le résultat de l'instruction qu'elle contient. Bien entendu, on peut rajouter des délimiteurs de block (accolades) aux arrow functions, auquel cas il faut utiliser return pour spécifier la valeur de retour :

var add = (first, second) => {

  return first + second;

}

Facile me direz-vous. Halte-là ! Il existe une différence essentielle, fondamentale, entre les fonctions classiques et les arrow functions : ces dernières sont scopées lexicalement (à la création),  tandis que les premières sont scopées dynamiquement (lors de l'utilisation). Autrement dit, la valeur de this est déterminée par l'endroit ou la fonction est créée, et non par l'endroit où la fonction est exécutée. De plus, la valeur de this ne peut pas être changée, elle reste la même pendant toute la durée de vie de la fonction. Illustrons cela avec un peu de code :

'use strict';

const dns = require('dns');

let myDomain = {

  name: 'example.com',

  ip: null,

  init: function() {

    dns.lookup(this.name, function(err, address) {

      if (!err) {

        this.ip = address;

      }

      callback();

    });

  }

}

myDomain.init();

Ce premier exemple va échouer, avec une exception de type TypeError (Cannot set property "ip" of null) sur l'instruction this.ip = address, car la fonction anonyme passée en callback à dns.lookup n'a pas de scope (de this) défini. Le même exemple, en revanche, fonctionne avec les arrow functions, car this est déterminé au moment de la création de la fonction, et est donc le même this que la méthode init :

'use strict';

const dns = require('dns');

let myDomain = {

  name: 'example.com',

  ip: null,

  init: function() {

    dns.lookup(this.name, (err, address) => {

      if (!err) {

        this.ip = address;

      }

      callback();

    });

  }

}

myDomain.init();

Il y a d'autres différences entre les fonctions classiques et les arrow functions :

- on ne peut pas utiliser new … avec les arrow functions ;

- les arrow functions n'embarquent pas l'objet spécial arguments ;

- on ne peut pas les nommer. Les arrow functions sont anonymes.

3.3 Classes

On arrive ici sur la fonctionnalité sans doute la plus polémique, et indispensable, de cette nouvelle version de JavaScript. C'est pourquoi, avant de rentrer dans le détail, il faut tout de suite préciser que l'héritage JavaScript reste prototypal. Les développeurs n'ont pas rajouté dans le langage un héritage « à la Java ». Cependant, ils ont repris la terminologie de class et extends.

3.3.1 Petite digression polémique

On comprend facilement les motivations qui ont poussé les membres de l'association Ecma International a mettre en place les classes dans le langage. J'en vois au moins deux.

La première, c'est d'avoir un système et fonctionnement unique pour gérer l'héritage dans JavaScript. En effet, depuis la nuit des temps, les librairies et frameworks implémentent le leur : Node.js bien sûr avec util.inherits (https://nodejs.org/docs/latest/api/util.html#util_util_inherits_constructor_superconstructor), mais aussi Ember.js (http://emberjs.com/api/classes/Ember.CoreObject.html), Backbone (http://backbonejs.org/#Model-extend), Mootools (http://mootools.net/core/docs/1.6.0/Class/Class), ExtJS (http://docs.sencha.com/extjs/6.0/6.0.2-modern/#!/api/Ext-method-define)… je m’arrête là. Et, me direz-vous, ces différents systèmes de classes et sous-classes sont-ils compatibles entre eux ? Bien sûr que non ! C'est, on en conviendra, un problème de taille.

La seconde, c'est que les étudiants du monde entier, dans le cadre scolaire, apprennent la programmation en Java. Les développeurs Java sont donc légion. Et maintenant que JavaScript devient incontournable, les industriels ont un problème: ces pauvres cerveaux formatés, depuis des décennies, à penser en classes mères, classes filles, interfaces, mettent un temps conséquent à trouver leurs marques lorsqu'on leur demande, ou qu'ils souhaitent, programmer en JavaScript. On comprend que la mise en place des paradigmes class et extends permet de faciliter la courbe d'apprentissage.

Et pourtant, ce n'est pas sans risque. Le comportement de l'héritage à la sauce Java est bien différent de l'héritage prototypal, et cela peut entraîner une fausse compréhension du langage, ce qui se traduit inévitablement par… des bugs.

3.3.2 Les bases

Une classe JavaScript se crée en utilisant le mot-clé class, et on définit le constructeur en définissant une méthode constructor :

class Magazine {

  constructor(topics) {

    this.topics = topics;

  }

  printTopics() {

    console.log(this.topics);

  }

}

var mag = new Magazine(['nodejs', 'javascript']);

mag.printTopics();

Il existe quelques différences entre les classes JavaScript et la manière historique (via function Mag() {} et Mag.protoype) de créer des classes :

- les déclarations de classes ne sont par hoistées ;
- le code à l'intérieur des classes est exécuté en strict mode, sans moyen de le désactiver ;
- les méthodes sont non-énumérables ;
- on ne peut pas appeler un constructeur de classe sans le mot-clé new devant ;
- on ne peut pas réutiliser le nom de la classe comme une variable à l'intérieur de la classe.

Les classes JavaScript permettent de créer des méthodes statiques, via le mot-clé static :

class Magazine {

  constructor(topics) {

    this.topics = topics;

  }

  static create(topics) {

    return new Magazine(topics);

  }

}

var mag = Magazine.create(['nodejs', 'javascript']);

mag.printTopics();

3.3.3 Les sous-classes (c'est super)

Les classes JavaScript supportent le sub-classing, et on accède aux méthodes de la classe parente via le mot-clé super :

class GLMF extends Magazine {

  constructor() {

    super(['gnu', 'linux', 'development']);

    super.printTopics();

  }

}

var mag = new GLMF();

Pour être tout à fait précis, le mot-clé super permet d'accéder au prototype parent de l'objet en cours.

Petite fonctionnalité amusante et qui prouve que JavaScript n'a rien perdu de sa souplesse, on peut déterminer dynamiquement l'héritage d'une classe.

function getImplem() {

  // merci d'imaginer un contenu très compliqué avec

  // plein de if else if elseelse if

  return class Magazine {

    constructor(topics) {

      this.topics = topics;

    }

  }

}

class GLMF extends getImplem() {

  constructor() {

    super(['gnu', 'linux', 'development']);

  }

}

var mag = new GLMF();

Conclusion

Il y aurait encore beaucoup à dire sur les nouveautés de cette cuvée 2015 de JavaScript : le système de modules (on se demande d'ailleurs si, et comment, Node.js va prendre le virage, ou continuera d'utiliser le maintenant universel require), les itérateurs et générateurs, qui permettent d'écrire du code asynchrone un peu comme s’il était synchrone, les symboles, ou encore les interpolations de chaînes de caractères… Mais aussi l'outillage associé, tels les transpileurs qui transforment le code ES2015 en ES5 afin de pouvoir l'utiliser dans votre Internet Explorer 9. J'espère en tout cas que cet apéritif vous aura donné envie d'explorer de fond en comble les exquis apports qui sont maintenant à portée de Node.

Tags : javascript, NodeJS