ES6 en détails est une série d’articles décrivant les nouvelles fonctionnalités ajoutées au langage de programmation JavaScript avec la sixième édition du standard ECMAScript (ES6 en abrégé).

La fonctionnalité dont je souhaiterais parler aujourd’hui est à la fois humble et étonnamment ambitieuse. Lorsque Brendan Eich a conçu la première version de JavaScript en 1995, il s’est trompé sur de nombreux sujets, certains font toujours partie du langage : l’objet Date, la conversion automatique des objets en NaN lorsqu’on les multiplie… Malgré ça, il a visé juste sur plein d’aspects fondamentaux : les objets, les prototypes, les fonctions comme entités de premier rang, les portées lexicales, la mutabilité par défaut. Le langage repose sur de bonnes bases, meilleures que quiconque le pensait au début.

Cependant, c’est sur une décision de conception particulière de Brendan que porte l’article d’aujourd’hui. Une décision dont on peut dire, je pense, qu’il s’agit d’une erreur. Une toute petite erreur, subtile. Vous pouvez avoir utilisé le langage pendant des années sans même l’avoir remarquée. Mais cette erreur est importante car elle fait partie des « bonnes parties » de JavaScript (NdT : en raison des écueils évoqués avant, JavaScript est souvent découpé entre les « bad parts », à éviter, et les « good parts », meilleurs morceaux du langage).

Cette erreur est liée aux variables.

Problème n°1 : les blocs ne forment pas de portées

La règle semble anodine : la portée d’une variable déclarée dans une fonction JS correspond au corps de la fonction dans son ensemble. Cela peut cependant avoir deux conséquences particulières qui font grincer des dents.

L’une est que la portée des variables déclarées dans les blocs ne correspond pas au bloc. C’est la fonction toute entière.

Vous n’aviez probablement jamais remarqué ça auparavant. J’ai bien peur que ce soit l’une des choses que désormais, vous ne pourrez plus oublier. Considérons un scénario où cela mène à un drôle de bug.

Disons que vous avez un bout de code existant qui utilise une variable nommée t :

function runTowerExperiment(tower, startTime) {
  var t = startTime;

  tower.on("tick", function () {
    ... code qui utilise t ...
  });
  ... plus de code ...
}

Tout fonctionne parfaitement, jusqu’ici. Maintenant, vous voulez ajouter des mesures de vitesse d’une boule de bowling, donc vous ajoutez une petite condition if à la fonction de rappel interne.

function runTowerExperiment(tower, startTime) {
  var t = startTime;

  tower.on("tick", function () {
    ... code qui utilise t ...
    if (bowlingBall.altitude() <= 0) {
      var t = readTachymeter();
      ...
    }
  });
  ... plus de code ...
}

Oh mon dieu ! Vous avez inconsciemment ajouté une seconde variable appelée t. Maintenant, dans le « code qui utilise t », qui fonctionnait bien avant, t fait référence à la nouvelle variable interne t à la place de la variable externe existante.

La portée d’une variable en JavaScript est comme l’outil Pot de peinture dans Photoshop. Elle s’étend dans les deux directions depuis la déclaration, en avant et en arrière, et jusqu’à ce qu’elle rencontre une délimitation de fonction. La portée de cette variable t allant loin en arrière, elle a été créée dès que nous sommes entrés dans la fonction. C’est ce qu’on appelle la remontée des variables (hoisting). J’aime me représenter une petite grue codée, actionnée par le moteur JS, qui fait monter chacune des variables et fonctions tout en haut de la fonction englobante.

Après, la remontée des variables a aussi ses bons côtés. Sans elle, plein de techniques parfaitement convenables qui fonctionnent bien avec la portée globale ne marcheraient plus dans une IIFE (Immediately-invoked function expression pour « expression de fonction immédiatement invoquée »). Dans le cas présent cependant, la remontée entraîne un gros bug : tous vos calculs utilisant t renverront NaN. Ce sera difficile à retrouver, trop même, en particulier si votre code est plus complexe que ce petit exemple gentillet…

On a ajouté un nouveau bloc de code et ça a entraîné une erreur dans du code situé avant ce bloc. C’est vraiment étrange, on s’attendrait vraiment à ce que les effets précèdent les causes.

Enfin ce problème est plutôt léger en comparaison du deuxième lié à var.

Problème n°2 : le partage excessif des variables dans les boucles

En utilisant ce code, vous pouvez deviner ce qui va se produire, c’est totalement logique :

var messages = ["Coucou", "je suis une page web", "alert() c'est rigolo !"];

for (var i = 0; i < messages.length; i++) {
  alert(messages[i]);
}

Si vous avez suivi cette série d’articles, vous savez que j’aime utiliser alert() pour les exemples de code. Vous savez peut-être aussi qu’alert() est une API assez horrible car elle est synchrone. Dès lors qu’une alerte apparaît à l’écran, les événements liés aux entrées de l’utilisateur ne sont plus déclenchés. Votre code JS (en réalité, toute votre interface utilisateur) est interrompu jusqu’à ce que l’utilisateur clique sur OK.

Bref, avec tout ça, alert() est un mauvais choix pour à peu près tout ce qu’on voudrait faire sur une page web. Je l’utilise car je pense que ces défauts font de alert() un bon outil d’apprentissage.

Je serais près à abandonner tout ça… si je pouvais faire parler un chat.

var messages = ["Miaou !", "Je suis un chat qui parle !", "Les callbacks c'est rigolo!"];

for (var i = 0; i < messages.length; i++) {
  setTimeout(function () {
    chat.miauler(messages[i]);
  }, i * 1500);
}

Regardez ce code (mal) fonctionner !

Il y a quelque chose qui cloche… Au lieu de dire les trois messages dans l’ordre, le chat dit « undefined » à trois reprises.

Voyez-vous la bogue ? bogue.jpg Crédit photo : Elzinga Alexander

Le problème est le suivant : il n’y a qu’une seule variable i. Elle est partagée par la boucle et les trois appels du callback pour le timeout. Lorsque la boucle a fini de s’exécuter, i vaut 3 (car messages.length vaut 3) et aucun des callbacks n’a encore été appelé à ce stade.

Aussi, lorsque le premier timeout se déclenche et appelle chat.miauler(messagesi), c’est messages[3] qui est utilisé. Celui-ci vaut, bien entendu, undefined.

Il existe de nombreuses manières pour corriger ce problème (en voici une). Ce problème provient avant tout des règles de portées qui s’appliquent à var. En fait, ce qu’on aurait aimé, c’est de ne jamais avoir eu ce problème.

Vous avez aimé let, vous allez adorer var

Pour une grande partie, les erreurs de conception de JavaScript ne peuvent pas être réparées (c’est valable également pour les autres langages de programmation mais ça s’applique particulièrement à JavaScript). La rétro-compatibilité implique de ne jamais changer le comportement du code JS qui existe sur le Web. Même le comité de standardisation ne peut pas dire « réglons ces bizarreries liées à l’insertion automatique de points-virgules ». Ceux qui implémentent les navigateurs refuseraient de le faire car cela serait trop punitif pour leurs utilisateurs.

Il y a dix ans, lorsque Brendan Eich décida de régler ce problème, il n’y avait qu’une seul solution envisageable.

Il ajouta un nouveau mot-clé, let, qui pourrait être utilisé pour déclarer les variables, comme var, mais qui utiliserait de meilleures règles pour les portées.

Ce mot-clé ressemble à :

let t = readTachymeter();

Ou encore :

for (let i = 0; i < messages.length; i++) {
  ...
}

let et var se comportent différemment donc si vous recherchez et remplacez toutes les occurrences de « var » par « let » dans votre code, cela pourrait casser certains endroits qui dépendent (probablement involontairement) des particularités de var. Toutefois, dans la majorité des cas, dans du code ES6, il est conseillé d’arrêter d’utiliser var et de passer à let. Certains disent ainsi que « let est le nouveau var ».

Quelles sont les différences exactes entre let et var ? Eh bien je suis ravi que vous ayez posé la question !

  • Les variables déclarées avec let ont une portée qui s’étend sur le bloc courant. La portée d’une telle variable correspond simplement au bloc dans lequel on se trouve et pas à la fonction englobante. Les variables sont toujours « remontées » mais plus à tort et à travers. Dans l’exemple précédent avec runTowerExperiment, utiliser let à la place de var suffit à régler le problème. Si vous utilisez let partout, vous n’aurez plus jamais ce genre d’erreur.

  • Les variables déclarées avec let dans la portée globale ne sont pas des propriétés de l’objet global. Autrement dit, on ne peut pas y accéder avec window.maVariable. Ces variables « vivent » dans la portée d’un bloc invisible qui englobe tout le code JavaScript qui s’exécute sur la page.

  • Les boucles de la forme for (let x ...) créent une nouvelle liaison pour x à chaque itération.

    Cette différence est assez subtile. Cela signifie que si un boucle for (let ...) est exécutée plusieurs fois et que cette boucle contient une fermeture (”closure”) (comme dans notre exemple avec le chat qui parle), chaque fermeture capturera une copie différente de la variable de boucle au lieu que chaque fermeture capture la même variable de boucle.

    Ainsi, l’exemple du chat peut lui aussi être corrigé en passant simplement de var à let.

    Cela s’applique aux trois boucles for : for-of, for-in et à la boucle for classique héritée de C et parée de ses trois points-virgules.

  • Si on utilise un variable let avant qu’elle soit déclarée, cela déclenchera une erreur. La variable n’est pas ”initialisée” tant que le flux d’instructions n’a pas atteint la ligne de code où la variable est déclarée. Par exemple :

    function update() {
      console.log("heure actuelle :", t);  // ReferenceError
      ...
      let t = readTachymeter();
    }
    

    Cette règle existe pour vous permettre de repérer plus facilement les bugs. Plutôt que d’avoir des résultats qui valent NaN, vous aurez une exception sur la ligne de code ayant causé le problème.

    Cette période, pensant laquelle la variable appartient à la portée mais n’est pas initialisée est appelée la « ”zone morte temporaire” ». J’espère qu’avec ce jargon, JavaScript inspirera bientôt des récits de science-fiction. Je n’ai rien lu de tel pour le moment.

    (Détails croustillants qui concernent les performances : dans la plupart des cas, vous pouvez déterminer si la déclaration a été faite rien qu’en regardant le code, de cette façon, le moteur JavaScript n’a pas à faire un test supplémentaire à chaque fois qu’on accède à la variable pour savoir si celle-ci a été initialisée. Malgré tout, dans une fermeture, ce n’est pas toujours évident. Dans ces situations, le moteur JavaScript effectuera une vérification lors de l’exécution. Cela signifie que let peut parfois être un soupçon plus lent que var.)

    (Détails croustillants sur les portées dans d’autres univers : dans certains langages, la portée de la variable débute à l’endroit où elle est déclarée plutôt que de s’étendre avant sur le début du bloc englobant. Le comité de standardisation a pensé à utiliser ce type de portée pour let. De cette façon, le t qui entraînait une ReferenceError n’aurait simplement pas appartenu à la portée ultérieure de let t et il n’aurait donc pas fait référence à cette variable. Il aurait même pu faire référence à une variable de la portée englobante. Toutefois, cette approche n’a pas bien fonctionné avec les fermetures et les remontées de fonctions, en fin de compte, elle fut abandonnée.

  • Redéclarer une variable avec let lève une exception SyntaxError.

    Cette règle existe également pour vous permettre de détecter les erreurs triviales. C’est cette différence qui risque d’avoir le plus d’impact si vous remplacez tous les var par let car cela s’applique également aux variables globales déclarées avec let.

    Si vous avez plusieurs scripts qui déclarent tous la même variable globale, vous feriez mieux de continuer à utiliser var pour ça. Si vous échangez avec let, le script qui sera chargé après le premier échouera avec une erreur.

    Sinon, vous pouvez utiliser les modules ES6… mais c’est une autre histoire.

(Détails croustillants sur la syntaxe : let est un mot-clé réservé en mode strict. En mode non-strict, pour garantir la rétrocompatibilité, vous pouvez toujours déclarer des variables, des fonctions et des arguments nommés let. Par exemple, vous pouvez tout à fait écrire : var let = "q"; ! En revanche, let let; n’est pas du tout autorisé.)

En dehors de ces différences, let et var sont assez semblables. Tous les deux permettent de déclarer plusieurs variables à la suite en les séparant par des virgules et tous les deux permettent d’utiliser la décomposition.

Il est à noter que les déclarations class se comportent comme let et non comme var. Si vous chargez un script qui contient une même classe définie plusieurs fois, vous aurez une erreur à la deuxième déclaration.

const

Bien, encore une chose !

ES6 introduit un troisième mot-clé qui peut être utilisé avec let : const.

Les variables déclarées avec const ressemblent à celles déclarées avec let à la seule différence qu’il est impossible d’affecter une valeur à une variable const autrement que lors de sa déclaration. Si on tente d’affecter une valeur à une variable const après sa déclaration, cela provoquera une SyntaxError.

const MAX_CAT_SIZE_KG = 3000; // 

MAX_CAT_SIZE_KG = 5000; // SyntaxError
MAX_CAT_SIZE_KG++; // bien essayé mais ca lève toujours une SyntaxError

Pour faire les choses bien, il est impossible déclarer une constante sans fournir de valeur.

const héhé;               // SyntaxError, coquinou va

Espace de noms de Zeus Marty !

« Les espaces de noms sont diablement formidables — utilisons-les plus encore ! » Tim Peters, The Zen of Python

Sous le capôt, les portées imbriquées sont un des concepts fondamentaux sur lesquels les langages de programmation sont construits. Ça fonctionne comme ça depuis ALGOL, une bagatelle d’environ 57 ans. Et c’est encore plus vrai aujourd’hui.

Avant ES3, JavaScript n’avait que les portées globales et les portées des fonctions (passons outre les instructions with s’il vous plaît). ES3 a introduit les instructions try-catch ce qui a impliqué l’ajout d’une nouvelle sorte de portée, utilisée uniquement pour les variables d’exceptions dans les blocs catch. ES5 a ajouté une portée qui est utilisée en mode strict avec eval(). ES6 ajoute les portées des blocs, les portées des boucles for, la nouvelle portée pour les variables let globales, les portées des modules et les portées additionnelles utilisées lorsque les valeurs par défaut sont évaluées avec les arguments.

Toutes ces portées supplémentaires ajoutées depuis ES3 sont nécessaires afin que les fonctionnalités procédurales et objets de JavaScript soient aussi souples, précises et intuitives que le sont les fermetures. Ces portées sont aussi là pour que toutes ces fonctionnalités interagissent sans problème avec les fermetures. Peut-être que vous n’aviez jamais remarqué ces règles de portées jusqu’à aujourd’hui. Si c’est le cas, cela signifie que le langage fait bien son travail.

Puis-je utiliser let et const dès maintenant ?

Oui. pour les utiliser sur le Web, vous devrez vous servir d’un compilateur ES6 compiler comm Babel, Traceur ou TypeScript (Babel et Traceur ne prennent pas encore en charge la zone morte temporaire).

io.js supporte let et const, mais seulement pour du code écrit en mode strict. Node.js le supporte aussi mais l’option —harmony est nécessaire.

Brendan Eich a mis en œuvre la première version de let dans Firefox il y a neuf ans. La fonctionnalité a été complètement repensée pendant le processus de normalisation. Shu-Yu Guo met à niveau notre implémentation pour correspondre à la norme, avec des revues de code par Jeff Walden entre autres.

Eh voilà, nous sommes dans la dernière ligne droite. La fin de notre navigation épique parmi les fonctionnalités ES6 est en vue. Dans deux semaines, nous allons en finir avec ce qui est probablement la fonctionnalité ES6 la plus attendue de toutes. Mais d’abord, la semaine prochaine, nous aurons un billet qui étend notre couverture antérieure d’une nouvelle fonctionnalité qui est tout simplement super. Alors s’il vous plaît rejoignez-nous parce qu’Eric Faust revient pour examiner à fond les sous-classes ES6 en détails.