Apprendre et comprendre JavaScript version ES6

ES7 est déjà dans nos chaumières et, en plus d'être à peu près au point avec ES5, vous n'avez toujours pas digéré ES6 ! Il va être temps de nous y pencher de plus près sur ce blog pour aborder la suite de l'aventure JavaScript sereinement. Cet article à pour but, de vous en apprendre plus sur la version ES5 tout en la comparant à des équivalences ES6 : cela nous permettra de comprendre en quoi ses améliorations peuvent nous aider au quotidien. Nous allons éplucher les fonctionnalités dans un ordre logique d'apprentissage et explorer les mécaniques sous-jacentes. Ça risque d'être long alors, de la même manière que cet article va être publié en plusieurs mise à jour, n'hésitez pas à le lire en plusieurs fois.

A curious JavaScript Story

C'est l'histoire d'un gars, Netscape, qui se dit : « ok, quand on va écrire des lignes dans un fichier, et qu'on va faire lire ce fichier à un programme, celui-ci va faire les trucs cool que les lignes lui disent de faire. » ; idée presque révolutionnaire puisque c'est déjà ce que fait n'importe quel programme concurrent. « ouais, mais moi, mon code il sera pas pré-transformé en charabia pour machine à l'avance, il sera lu et pris en compte directement ! ».

Notre Nets l'invente pas pour rien ce programme. Son idée c'est de le placer dans son navigateur qui permet de surfer sur Internet pour faire des trucs cool dedans comme interagir avec le contenu d'une page ; ce qu'il fait. Le gars sait pas trop comment appeler ça et fini par l'appeler JavaScript car à l'époque, ça lui permet de se faire un peu de publicité sur le dos de son collègue Java (ou inversement proportionnel).

L'idée du gars est tellement cool que Microsoft se dit qu'il va faire pareil et appel ça du JScript (histoire de se faire de la pub aussi quoi) et de le placer dans son navigateur fétiche à lui, appelé Internet Explorer. Mais Nets a autant les boules qu'il est sympa et décide, pour éviter que d'autres gars comme Mic fasse n'importe quoi avec son JavaScript de demander à son pote Ecma International de son petit nom Ecma d'expliquer et d'user de son réseau pour faire savoir que le JavaScript « ça marche comme ça, et pas autrement », chose que Ecma fait et consigne dans un papier sobrement intitulé Standard ECMA-262. Dans ce papier naît alors le nouveau standard ECMAScript. Grâce à ça, des mecs comme Adobe peuvent inventer des trucs sympa comme le Flash et les gens comme Mic sont censé arrêter de faire n'importe quoi.

Nets était fière se son JavaScript car il avait inventé un « modèle objet orienté prototype » méga performant pour du langage de script. Après moult rebondissement cela c'est soldé par une version ECMASript 3. On va l'appeler ES3 pour faire plus court. Cependant, des proches de Ecma, la Team4, qui avaient pas trop pigé le coup du prototypage, décida de faire évoluer la spécifications pour que ça ressemble plus à de l'objet fondé sur les classes comme ces bons langages compilés (miam !) et d'en faire à terme ES4.

Mais holà ! Les puristes se sont levés, la Team5. La Team5 a dit « l'ES, c'est avant tout du prototypage ! De diou ! » ce qui l'a mena a écrire en parallèle la même chose, mais en pas pareil, afin d'en faire à terme l'ES5 que nous avons tous connu et qui vit dans Chrome, Firefox, Safari, Opera, et feu (en fait pas encore) Internet Explorer. Notre Mic a beau dire qu'il « a compris qu'il faut suivre les spécifications de Ecma », il galère quand même pas mal en ce traînant ses versions antérieurs incrustés à la cloueuse. Mic, ES4, ES5, etc. : ils perdent tout le monde. Les gens inventent des abstractions en JavaScript pour faire du JavaScript qui marche chez tout le monde. Les gars « nous ont aime les prototypes » se sont mis à chier des bibliothèques comme Prototype (qu'on comprenne bien qu'ils aiment les prototypes) tandis que les gars « nous ont aime les classes » se sont mis à chier des transpileurs et truc dans le genre parce que en vrai quand tu croises un type faible, mais dynamique, tu te dis que le gars est louches. Du coup, maintenant le « gars qui passe par là » jette un œil à tout ce beau monde et se dit : « et bah putain... ».

ES6 est alors en marche et réconcilie gentiment les gars du prototypage et des classes en prenant le partie suivant : « on va dire que ça marche par prototype ok, mais on va faire en sorte de planquer ça sous de la classe ! Malin, hein ? ».

Par dessus ça, y a un mec, Joyent, qui se dit que même si JavaScript a besoin d'un hôte pour fonctionner, pourquoi ça serait forcément un navigateur ; l'hôte ? « Et si j'invitais ce bon vieux JavaScript à travailler... sur mon OS ? Que dis-je sur tous les OS ! ». Hop là ! Un peu de HTTP par ci, un peu de lecture/écriture de fichier par là et plop : Node.js est né. Du JavaScript côté serveur. À ce niveau, je vous parle pas du « gars qui passe par là » qui lui à surement abandonner l'histoire...

En tout cas Joy se laisse pas abattre et implémente ES6 en parallèle du fonctionnement de ES5 dans Node.js. Le gars il s'en fou ; il est tout seul, il fait ce qu'il veut et surtout : il suit scrupuleusement les bon conseil d'Ecma contrairement à un certain Mic. Mic, qui clame haut et fort à présent que « si, si. dans mon Edge, maintenant, je fais tout bien » ; mais sans succès.

Résultat des courses ? HTML5 propulse JavaScript, Node.js propulse JavaScript, ES6 propulse JavaScript et le train roule sacrément vite...

Il est peut-être venu le temps d'essayer de rattraper les wagons si vous êtes resté sur le quai !

Liste des fonctionnalités ES6

Le schéma sera le suivant : on tente d'expliquer ce qu'on aurait pu faire en ES5, on le fait en ES6 en constatant les différences et en levant la valeur ajoutée.

Constantes const

Constants > Constants

ES6 ajoute un support pour les variables immuables. Sous ce jolie nom se cache simplement le concept de constante : une variable à laquelle aucun nouveau contenu ne peut être assigné après sa déclaration (en utilisant l'opérateur d'affectation à gauche =). À noter que c'est le conteneur et non le contenu qui est immuable, cela signifie qu'un objet qui serait affecté à une variable immuable ne peut pas être remplacé (sa référence ne peut pas être changée), mais son contenu lui, peut bouger (ajout, modification et suppression de propriétés). Un petit rappel entre les objets qui sont stockées par référence et les primitives qui sont directement stocké se trouve ici.

En ES5 : D'après cette description, un des moyens de créer une constante avec ES5 est d'utiliser la fonction Object.defineProperty(). Cette fonction permet d'attribuer à des propriétés d'objet un comportement immuable. Créons ainsi la constante PI.

>  /** 
    * `PI` en tant que variable local ou 
    * `window.PI` en tant que variable global n'existe pas.
    */
   PI;
<· « Uncaught ReferenceError: PI is not defined »
>  /**
    * On va créer une propriété se comportant « comme » une constante.
    * p1 : Affectation de la propriété à l'objet global `window` (navigateur)
    *      ou à l'objet global `global` (node.js).
    * p2 : Définition de la clé de la propriété en tant que l'opérande `"PI"`.
    * p3 : Options attribuant sa valeur avec l'opérande `3.141593`, 
    *      expliquant qu'il apparaît lors de l'utilisation d'une boucle et surtout
    *      qu'aucun opérande ne peut-être affectée après sa définition
    */
   Object.defineProperty(window ? window : global, "PI", {
       value:        3.141593,
       enumerable:   true,
       writable:     false
   });
<· Window {…}
>  /* `PI` est maintenant défini et vaut `3.141593`. */
   PI;
<· 3.141593
>  /** 
    * Affecter `PI` va effectivement renvoyer 
    * l'opérande que vous avez choisis d'affecter. 
    */
   PI = "Nombre PI";
<· "Nombre PI"
>  /* Cependant la valeur `PI` n'aura pas bougée d'un iota. */
   PI;
<· 3.141593
>  /** 
    * Ce qu'il faut bien comprendre dans notre cas de figure 
    * c'est que PI est une variable globale (ou propriété de l'objet global). 
    */
   PI === window.PI;
<· true

En ES6 : Le moyen de réellement créer une constante est d'utiliser l'opérateur const.

>  /** 
    * `PI` en tant que variable local ou 
    * `window.PI` en tant que variable global n'existe pas.
    */
   PI;
<· « Uncaught ReferenceError: PI is not defined »
>  /**
    * On va créer une constante `PI` avec la valeur `3.141593`.
    */
   const PI = 3.141593;
<· undefined
>  /* `PI` est maintenant défini et vaut `3.141593`. */
   PI;
<· 3.141593
>  /** 
    * Affecter `PI` va cette fois lancer une exception. 
    */
   PI = "Nombre PI";
<· « Uncaught TypeError: Assignment to constant variable. »
>  /** 
    * Cependant ce n'est ici pas l'équivalence de notre exemple ES5 car ici
    * `PI` est une variable locale immuable (constante) et 
    * non une propriété de l'objet global. 
    */
   PI === window.PI;
<· false

En conclusion l'opérateur const qui créé une constante (une variable locale immuable) n'est pas la même chose que la fonction Object.defineProperty() qui créé une propriété d'objet immuable, une sorte de propstante !

Variable à portée limitée let

Scoping > Block-Scoped Variables

Contrairement à beaucoup de langage, en JavaScript ES5, la portée des variables déclarées avec l'opérateur var n'est pas limitée à un bloc d'accolade. Ainsi, si je défini une variable à l'intérieur d'une instruction de contrôle de flux comme if, ma variable sera également disponible à l'extérieur. Cela n'est plus le cas si ma variable est déclarée avec let (et également avec const). Cela signifie qu'une variable déclarée dans l'instruction if ou for n'existe pas en dehors.

En ES5 : Voici le comportement induit par les variables déclarées avec l'opérateur var que ce soit entre les accolades mais également dans les boucles.

>  if ("1" == 1) {
       /* Bien qu'elle soit créée dans un bloc... */
       var str = "Hello World";
   }
   /* ...la variable `str` est accessible en dehors de ce bloc. */
   str;
<· "Hello World"
>  if ("1" == 1) {
       /* Seules les fonctions enferment la portée des variables... */
       (function () {
           var str = "Hello World";
       }());
   }
   /* ...et les empêches d'être accessible de l'extérieur `str`. */
   str;
<· « Uncaught ReferenceError: str is not defined »
>  /** 
    * Le fait que les variables ne soit pas limitées aux accolades empêche 
    * cette liste de fonction de fonctionner comme on le souhaiterait.
    */
   var callbacks = [];
   for (var i = 0; i < 3; i++) {
       /** 
        * Ainsi le problème est que à l'instant ou chaque fonction 
        * est assignée dans le tableau `callbacks` à l'indice `i`, 
        * `i` vaut la valeur actuel de la boucle.
        */
       callbacks[i] = function() { return i * 2; };
   }
   /** 
    * Cependant, lorsque `callbacks[0]()`, `callbacks[1]()` et `callbacks[2]()` sont exécutées
    * la valeur de `i` est de `3` ce qui donne pour l'exemple courant au lieu de `0 2 4`...
    */
   callbacks[0]() + " " + callbacks[1]() + " "  + callbacks[2]();
<· 6 6 6
>  /** 
    * Pour solutionner le problème il faut faire un instantané 
    * de la valeur de `i` en passant i en paramètre d'une fonction, 
    * ce qui copie sa valeur et la conserve pour la suite puisque 
    * `i` est une primitive et que les primitives sont assignée par copie.
    */
   var callbacks = [];
   for (var i = 0; i < 3; i++) {
       (function (i) {
           /* Ainsi le paramètre `i` est figé à sa valeur lors de son assignation. */
           callbacks[i] = function() { return i * 2; };
       }(i));
   }
   /* Et le résultat est cette fois bien celui attendu. */
   callbacks[0]() + " " + callbacks[1]() + " "  + callbacks[2]();
<· 0 2 4

En ES6 : Tous nos problèmes sont résolus ici par ce fameux let. Non seulement celui-ci nous permet de vraiment cloisonner une variable dans un bloc, mais également de créer des boucles sans gérer nous même le cloisonnement de la variable d'itération.

>  if ("1" == 1) {
      /* Cette fois ci une variable créée dans un bloc... */
      let str = "Hello World";
   }
   str;
   /* ...n'est pas accessible en dehors de ce bloc. */
<· « Uncaught ReferenceError: str is not defined »
>  /** 
    * Et le fait que les variables soient cette fois limitées aux accolades, et donc
    * à un seul tour de boucle permet à notre liste de fonctionner comme prévue.
    */
   var callbacks = [];
   for (let i = 0; i < 3; i++) {
       /* Grâce à `let`, `i` est un instantané à chaque tour de boucle. */
       callbacks[i] = function() { return i * 2; };
   }
   /* Cela permet à nos fonctions de fonctionner comme souhaitez. */
   callbacks[0]() + " " + callbacks[1]() + " "  + callbacks[2]();
<· 0 2 4

En conclusion l'opérateur let est parfait pour permettre des variables locales à un bloc d'être consommées sans impacter le reste du champ lexical. Il permet également de créer des instantanés à chaque tour de boucle pour éviter le cloisonnement.

Fonction à portée limitée {}

Scoping > Block-Scoped Functions

De la même manière qu'il est possible de limiter une variable à un block en ES6, il est également possible de limiter une fonction à un block. Cela grâce à l'ajout des accolades { et } autour de la zone dont les fonctions doivent être à portée limitée.

En ES5 : Voici le comportement d'une fonction définie puis redéfini dans un block.

>  /* Si je définie une variable dans le flux... */
   function test() { return "external"; }
   /* ...puis que je la redéfini ensuite dans un block... */
   {
       function test() { return "internal"; }
   }
   /**
    * ... `test()` va retourner `"internal"` puisqu'en JavaScript 
    * les blocks ne cloisonnent pas les variables et fonctions.
    */
   test();
<· "internal"
>  function test() { return "external"; }
   /**
    * Il faut faire appel à une fonction anonyme auto-exécutée
    * pour induire un nouveau champ lexical et cloisonner
    * la valeur.
    */
   (function () {
       function test() { return "internal"; }
   }());
   test();
<· "external"

En ES6 : Cependant avec ES6, il est possible de limitée la portée des fonctions déclaré à un block et non à une fonction en entourant le tout des accolades { et }.

>  /* Si j'englobe les fonctions avec des accolades... */
   {
       function test() { return "external"; }
       /* ...les futures accolades internes seront cloisonnante... */
       {
           function test() { return "internal"; }
       }
       /**
        * ... et ainsi la fonction redéfini dans un sous block 
        * ne sera pas prise en compte dans le block du dessus.
        */
       test();
   }
<· "external"
>  /** 
    * Il faut bien comprendre que ce comportement ne fonctionne qu'avec
    * le mot-clé `function` aussi pour les variables, il faut utiliser `let`
    */
   {
       var withVar = "var external";
       let withLet = "let external";
       {
           var withVar = "var internal";
           let withLet = "let internal";
       }
       withVar + " | " + withLet;
   }
<· "var internal | let external"

En conclusion les fonctions cloisonnées permettent de limiter l'utilisation d'une fonction à un block sans créer de nouveau champ lexical. Le fait que ce mécanisme soit limité au mot clé function le rend difficilement appréhendable tout de même.

Valeurs de paramètre par défaut

Extended Parameter Handling > Default Parameter Values

Quand on déclare une fonction en JavaScript, on indique les paramètres qu'on lui passe entre ses parenthèses et... c'est tout. On ne précise pas le type attendu, on ne précise pas de valeur par défaut. Si on souhaite faire cela afin d'indiquer de quel type est une variable ou d'attribuer une valeur autre que undefined en cas de non passage de paramètre, il faut le faire à la main !

Ça c'était avant, car ES6 implémente les paramètres auxquelles ont peut passer des valeurs par défaut en cas d'absence lors de l'appel.

En ES5 : Par défaut, une valeur non passé s'attribut la valeur de type Undefined : undefined.

>  function fn(str, nbr, bool, obj) {
       return {
           str:  str,
           nbr:  nbr,
           bool: bool,
           obj: obj
       };
   };
   fn("Hello World");
<· Object {str: "Hello World", nbr: undefined, bool: undefined, obj: undefined}

Pour donner plus d'indication sur les valeurs à passer, ou affecter une valeur différente de undefined aux paramètres non passé on fait donc ainsi :

>  function fn(str, nbr, bool, obj) {
       str  = str || "";
       nbr  = nbr || NaN;
       bool = bool || false;
       obj = obj || null;
       return {
           str:  str,
           nbr:  nbr,
           bool: bool,
           obj: obj
       };
   };
   fn("Hello World");
<· Object {str: "Hello World", nbr: NaN, bool: false, obj: null}

En ES6 : Cependant avec ES6, il est directement possible d'affecter ses valeurs lors de la déclaration des paramètres.

>  function fn(str = "", nbr = NaN, bool = false, obj = null) {
       return {
           str:  str,
           nbr:  nbr,
           bool: bool,
           obj: obj
       };
   };
   fn("Hello World");
<· Object {str: "Hello World", nbr: NaN, bool: false, obj: null}

Portée de fonction étendue =>

Arrow Functions > Lexical this

L'utilisation de this est un très vaste chapitre. Je vous renvoi à cet article pour en comprendre toute sa portée. Ici nous allons étudier son comportement lorsqu'il est appelé à l'intérieur d'une fonction de retour.

En cours de rédaction...