Tutoriel présentant les nouveautés ECMAScript 6

Les variables et les fonctions

Maurice Chavelli

JavaScript existe depuis longtemps et il est resté longuement cantonné à la dynamisation des pages web. Mais depuis quelques années, il sort de ce rôle et se déploie avec efficacité dans d’autres contextes. Par exemple, son utilisation côté serveur avec node.js est un franc succès.‌

Officiellement, JavaScript se nomme ECMAScript et il est normalisé. On trouve les dernières fonctionnalités et définitions dans la norme Ecma-262.

Dans ce cours, je vous présente ES6 qui conserve l’existant (donc tout ES5 reste valable) et ajoute de nombreuses et intéressantes possibilités, en particulier les modules et les classes.‌

Ce cours n’est pas une initiation à JavaScript, mais s’adresse à ceux qui savent déjà coder avec ES5 et qui désirent aller plus loin avec les nouvelles possibilités d’ES6.

Commentez

- Titre : Tutoriel présentant les nouveautés ECMAScript 6

- Auteur : Maurice Chavelli

- Parution : 26 décembre 2018

- Licence : Copyright ® 2018 Laravel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. ES6 : les variables

- I-A. Déclaration avec « var »

- I-A-1. Portée des variables

- I-A-2. Remontée des variables

- I-A-3. Déclarations multiples

- I-B. Déclaration avec « let »

- I-B-1. Portée des variables

- I-B-2. Pas de déclarations multiples

- I-C. Déclaration avec « const »

- I-D. La zone morte temporelle

- I-E. En résumé

- II. ES6 : les paramètres des fonctions

- II-A. Les valeurs par défaut des paramètres

- II-A-1. Une valeur par défaut

- II-A-2. Une valeur de paramètre comme valeur par défaut

- II-A-3. Une fonction comme valeur par défaut

- II-B. Le paramètre du reste

- II-C. L'opérateur de décomposition (spread operator)

- II-D. En résumé

- III. ES6 : les fonctions

- III-A. Le nom des fonctions

- III-A-1. Les méthodes d'un objet

- III-B. Portée des fonctions

- III-C. Les fonctions fléchées

- III-C-1. La syntaxe

- III-C-2. La valeur de this

- III-D. En résumé

I - ES6 : les variables

La portée des variables en JavaScript peut surprendre lorsqu'on est habitué à d'autres langages. Dans ce chapitre on va faire un peu le point en montrant ce qu'apporte ES6 à ce niveau avec l'introduction des mots-clés let et const.

I-A - Déclaration avec « var »

I-A-1 - Portée des variables

Avec ES5 on dispose du mot-clé var pour déclarer les variables. Selon où on fait la déclaration la variable n'a pas la même portée :

- déclaration en dehors d'une fonction : portée globale, donc disponible partout ;

- déclaration dans une fonction : portée locale donc disponible uniquement dans la fonction.

Jusque-là tout semble pour le mieux dans le meilleur des mondes informatiques, mais…

Si on déclare une variable dans une instruction de bloc elle reste disponible en dehors de ce bloc :

1. function setAge(age) {

2. if (age > 18) {

3. var adult = true;

4. }

5. console.log(adult);

6. }

7. setAge(22)

Si vous exécutez le code ci-dessus vous allez obtenir ce résultat :

true

Autrement dit la variable adult définie dans le bloc reste disponible en dehors de ce bloc.

La portée va même sans doute plus loin que vous ne le pensez, pour une portée globale, parce que la variable est accessible dans l'objet global qui est window pour une page web :

var age = 20;
console.log(age);  // 20
console.log(window.age);  // 20

Quand on utilise var on peut donc écraser une variable de l'objet global !

I-A-2 - Remontée des variables

Pour comprendre le fonctionnement du code qu'on a vu ci-dessus il faut savoir comment procède JavaScript avec le mot-clé var. En gros il va traduire le code ainsi :

1. function setAge(age) {

2. var adult;

3. if (age > 18) {

4. adult = true;

5. }

6. console.log(adult);

7. }

8. setAge(22);

Il va remonter la déclaration de la variable au début de la fonction et donc cette variable est disponible dans toute la fonction !

Un effet détourné de cette remontée est qu'on peut utiliser une variable qui est déclarée plus tard.

I-A-3 - Déclarations multiples

Avec le mot-clé var rien n'empêche de déclarer plusieurs fois une variable avec le même identifiant :

var a = 2;
var a = 3;
console.log(a);  // 3

I-B - Déclaration avec « let »

I-B-1 - Portée des variables

ES6 introduit le nouveau mot-clé let.

Avec le mot-clé let une variable a comme portée le bloc où elle est déclarée.

Si on reprend le code vu ci-dessus avec let :

1. function setAge(age) {

2. if (age > 18) {

3. let adult = true;

4. }

5. console.log(adult);

6. }

7. setAge(22)

Cette fois on ne va rien obtenir parce qu'en dehors du bloc la variable adult n'existe pas.

D'autre part, contrairement à var, la portée ne concerne plus l'objet global :

1. window.age = 10;

2. let age = 20;

3. console.log(age);  //20

4. console.log(window.age);  // 10

Ainsi on ne crée pas une propriété dans l'objet global, ce qui est plus sûr !

I-B-2 - Pas de déclarations multiples

Contrairement au mot-clé var, avec let on n'a pas le droit de faire une nouvelle déclaration avec le même identifiant dans le même bloc :

let a = 2;
let a = 3;  // Erreur !

Par contre, dans des blocs différents rien n'empêche d'utiliser un même identifiant même s'il vaut mieux l'éviter pour des raisons de clarté de code.

I-C - Déclaration avec « const »

Avec ES6 il est maintenant possible de déclarer une constante, donc une variable dont on ne peut pas changer la valeur, avec le mot-clé const :

const maConstante = 20;

Il faut obligatoirement donner une valeur à l'initialisation d'une constante !

La règle de portée est la même que pour let : le bloc de déclaration.

On ne peut pas non plus faire de nouvelle déclaration avec la même portée.

Lorsqu'on dit qu'on ne peut pas changer la valeur d'une constante il faut apporter une petite précision : c'est vrai si on a une valeur, ça ne l'est plus si on a un objet. Dans ce cas c'est la référence qu'on ne peut pas changer.

Vous avez donc le droit d'écrire ce genre de code :

1. const voiture = {

2. marque: "Renault"

3. };

4. voiture.marque = "BMW";

5. console.log(voiture);  // {"marque":"BMW"}

Vous changez la valeur d'une propriété, pas la référence de l'objet.

Par contre vous n'avez pas le droit d'écrire ceci :

1. const voiture = {

2. marque: "Renault"

3. };

4. voiture = {

5. marque: "Fiat"

6. };

Parce que là vous changez la référence.

Pour geler ‌un objet, donc le rendre non modifiable, il faut utiliser la méthode freeze :

Object.freeze(voiture);

Une bonne pratique consiste à utiliser const par défaut et uniquement let quand on est sûr que la valeur de la variable doit changer (ou la référence si on a un objet).

I-D - La zone morte temporelle

On a vu ci-dessus que lorsqu'on utilise le mot-clé var la déclaration de la variable est « remontée » par JavaScript quel que soit l'emplacement effectif de la déclaration.

Lorsqu'on utilise let ou const ce n'est plus vrai !

Autrement dit, on ne peut pas utiliser une variable avant qu'elle soit déclarée avec let ou const.

console.log(age);  // undefined
let age = 10;  // idem avec const

Lorsque JavaScript analyse le code et trouve un bloc, s'il rencontre une déclaration avec let ou const, il la place dans la zone morte temporelle (temporal dead zone) où elle est inaccessible jusqu'à ce que le flot d'exécution arrive effectivement sur la déclaration.

I-E - En résumé

- Lorsqu'on utilise le mot-clé var la variable n'est pas limitée à son bloc de déclaration, elle est « remontée » au début du code et sa portée peut concerner l'objet global.

- L'utilisation du mot-clé let limite la portée d'une variable à son bloc de déclaration, d'autre part, elle n'atteint jamais l'objet global.

- Le mot-clé const permet de déclarer une constante avec les mêmes règles de portée que le mot-clé let.

- La zone morte temporelle stocke les déclarations effectuées avec let et const jusqu'à l'exécution du code de la déclaration.

II - ES6 : les paramètres des fonctions

Gérer des valeurs par défaut pour les paramètres des fonctions avec ES5 n'est pas prévu et il faut s'en sortir avec des astuces. D'autre part, que se passe-t-il quand on veut passer plus de paramètres que prévu par la fonction ? Là aussi avec ES5 on doit recourir à certaines stratégies assez lourdes.

On va voir que ES6 résout avec élégance ces deux problèmes.

II-A - Les valeurs par défaut des paramètres

II-A-1 - Une valeur par défaut

ES5 ne prévoit pas de valeur par défaut pour un paramètre absent, alors il faut s'en sortir autrement, une manière de faire est celle-ci :

1. function ajoute(a, b) {

2. b = b || 10;

3. return a + b;

4. }

5. console.log(ajoute(2)); // 12

L'utilisation de l'opérateur OR permet d'affecter une valeur par défaut parce qu'il retourne automatiquement le second élément si le premier est null ou undefined.

Il y a toutefois un petit souci si on transmet la valeur 0 ou une chaîne vide parce que dans les deux cas la valeur est considérée comme false.

Pour contourner ce problème on préfère utiliser cette stratégie :

1. function ajoute(a, b) {

2. if (b === undefined) b = 10;

3. return a + b;

4. }

5. console.log(ajoute(2, 0)); // 2

Cette fois ça fonctionne bien même en transmettant la valeur 0.

Mais reconnaissez que ce n'est quand même pas très élégant !

Avec ES6 ça devient plus simple et facile :

1. function ajoute(a, b = 10) {

2. return a + b;

3. }

4. console.log(ajoute(2)); // 12

On se retrouve avec une syntaxe classique qu'on a dans pratiquement tous les autres langages.

II-A-2 - Une valeur de paramètre comme valeur par défaut

Regardez maintenant le code un peu modifié :

1. function ajoute(a, b = a) {

2. return a + b;

3. }

4. console.log(ajoute(2)); // 4

Pour le deuxième paramètre on prend la valeur du premier si aucune valeur n'est transmise.

On ne pourrait pas faire l'inverse à cause de l'ordre des définitions.

II-A-3 - Une fonction comme valeur par défaut

On peut aussi définir la valeur par défaut avec une fonction :

1. function valeur() {

2. return 10;

3. }

4. function ajoute(a, b = valeur()) {

5. return a + b;

6. }

7. console.log(ajoute(2)); // 12

Ce qui permet de faire des choses intéressantes en combinant avec la possibilité vue ci-dessus :

1. function valeur(a) {

2. return a * 2;

3. }

4. function ajoute(a, b = valeur(a)) {

5. return a + b;

6. }

7. console.log(ajoute(2)); // 6

Ça vous donne des idées ?

II-B - Le paramètre du reste

Avec ES5 on peut passer plus de paramètres que prévu dans une fonction. On dispose de l'objet arguments pour gérer cette situation. Voici un code d'exemple :

1. function ajoute(a) {

2. console.log(arguments.length); // 4

3. var total = 2 * a;

4. for (var i = 1; i < arguments.length; i++) {

5. total += arguments[i];

6. }

7. return total;

8. }

9. console.log(ajoute(1, 2, 3, 4)); // 11

On prend le double du premier paramètre et on ajoute tous les suivants.

Avec ES6 on dispose du paramètre du reste qui utilise la syntaxe des trois points :

1. function ajoute(a, ...valeurs) {

2. let total = 2 * a;

3. for (let i = 0; i < valeurs.length; i++) {

4. total += valeurs[i];

5. }

6. return total;

7. }

8. console.log(ajoute(1, 2, 3, 4)); // 11

L'argument valeurs devient un tableau qui contient les paramètres passés à la fonction après les paramètres nommés.‌

Il ne peut y avoir qu'un seul paramètre du reste et ce doit être le dernier, ce qui est logique…

II-C - L'opérateur de décomposition (spread operator)

L'opérateur de décomposition utilise la même syntaxe que le paramètre du reste mais il est son opposé.

On a vu que l'opérateur du reste transforme les arguments restants en tableau. Quant à lui, l'opérateur de décomposition transforme un tableau en arguments pour une fonction.

Un bon exemple est celui de la méthode Math.max. Cette méthode retourne la plus grande valeur parmi les arguments transmis :

console.log(Math.max(-10, 6, 12, 4)); // 12

Mais vous ne pouvez pas transmettre un tableau à cette méthode.

Avec l'opérateur de décomposition ça devient facile :

console.log(Math.max(...[-10, 6, 12, 4])); // 12

Vous pouvez utiliser cet opérateur aussi dans un tableau :

console.log(Math.max(...[-10, ...[6, 12], 4])); // 12

II-D - En résumé

- Avec ES6 on peut prévoir des valeurs par défaut pour les paramètres des fonctions.

- Avec ES6 on dispose du paramètre du reste pour gérer les arguments supplémentaires.

- Avec ES6 on dispose de l'opérateur de décomposition pour transformer un tableau en arguments pour une fonction.

III - ES6 : les fonctions

Les fonctions sont les pièces maîtresses de JavaScript, à tel point qu'il existe même une programmation fonctionnelle.

Dans ce chapitre on va voir les nouveautés de ES6 pour les fonctions : noms, portée et fonctions fléchées.

III-A - Le nom des fonctions

Il est souvent utile de connaître le nom d'une fonction, ne serait-ce que lorsqu'on débogue du code. Avec ES5 il existe une propriété name qui contient ce nom (mais non pris en charge par tous les moteurs) :

1. function toto() {function toto() {};

2. var titi = function() {};

3. console.log(toto.name);  // toto

4. console.log(titi.name);  // titi

Quel est le nom d'une fonction anonyme ?

Par principe elle n'en a justement aucun parce qu'on n'a jamais à l'appeler, mais on peut l'affecter à une variable comme on l'a fait pour titi ci-dessus, dans ce cas elle porte le nom de la variable.

Avec ES6 toutes les fonctions ont un nom, donc la propriété name est toujours renseignée.

III-A-1 - Les méthodes d'un objet

On peut se demander comment sont nommées les méthodes d'un objet. Voyons avec un exemple :

1. let objet = {

2. methode1() {},

3. methode2: function () {}

4. };

5. console.log(objet.methode1.name); // methode1

6. console.log(objet.methode2.name); // methode2

Très logiquement on se retrouve avec le nom des méthodes.

Mais il y a le cas particulier des accesseurs (getters) et mutateurs (setters) :

1. let objet = {

2. get name() {},

3. set name(value) {}

4. };

5. console.log((Object.getOwnPropertyDescriptor(objet, 'name').get).name); // "get name"

6. console.log((Object.getOwnPropertyDescriptor(objet, 'name').set).name); // "set name"

On voit que le nom commence avec set ou get.

Le nom d'un fonction a seulement un caractère informatif, il ne peut pas être utilisé dans le code. Sa principale utilité concerne le débogage.

III-B - Portée des fonctions

Avec ES5 en mode strict on ne peut pas placer une fonction dans un bloc :

{
function f() {};
}

Par contre on peut avoir une expression de fonction :

{
var f = function () {};
}

Avec ES6 on peut placer une fonction dans un bloc, elle est alors accessible uniquement dans ce bloc.

Et évidemment dans le cas d'une expression de fonction on va utiliser let pour limiter la portée au bloc.

III-C - Les fonctions fléchées

Abordons maintenant une des nouveautés les plus intéressantes de ES6 : les fonctions fléchées.

Comme leur nom l'indique on utilise une nouvelle syntaxe avec une flèche =>‌

On va voir qu'on a deux avantages par rapport aux expressions de fonctions classiques :

- une syntaxe courte ;

- un this lexical.

III-C-1 - La syntaxe

Mais commençons par voir la syntaxe d'une fonction fléchée :

let resultat = valeur => valeur;
console.log(resultat(2));  // 2

J'ai défini la fonction resultat, je lui transmet une valeur et elle se contente de retourner cette valeur.

Bon, elle ne sert à rien ma fonction mais c'est juste pour voir la syntaxe. En version ES5 on aurait ceci :

var resultat = function resultat(valeur) {
return valeur;
};

On va aller un peu plus loin en utilisant deux paramètres :

let resultat = (valeur1, valeur2) => valeur1 * valeur2;
console.log(resultat(2, 3));  // 6

Cette fois ma fonction est utile, elle retourne le produit des deux valeurs transmises. En version ES5 on aurait ceci :

var resultat = function resultat(valeur1, valeur2) {
return valeur1 * valeur2;
};

On voit que la syntaxe est bien plus concise et claire avec une fonction fléchée.

Si on a un peu plus de traitement à effectuer alors on utilise des accolades et return :

1. let resultat = (valeur1, valeur2) => {

2. if(valeur1 > valeur2) {

3. return valeur1;

4. } else {

5. return valeur2;

6. }

7. }

8. console.log(resultat(2, 3));  // 3

III-C-2 - La valeur de this

Avec ES5 la valeur de this est déterminée de façon dynamique et dépend du contexte (constructeur, fonction, méthode…), ce qui n'est pas sans surprises et difficultés.

Avec ES6 la valeur de this est déterminée de façon lexicale, autrement dit elle est déterminée par le contexte englobant.

Bon, si ce n'est pas très clair on va prendre des exemples !

Je veux créer un indexeur avec le choix du numéro de départ, voici le code que j'essaie :

1. function Numerote(depart) {

2. this.depart = depart;

3. }

4. Numerote.prototype.numeroteTableau = function (tableau) {

5. return tableau.map(function (x) {

6. return x + this.depart++;

7. });

8. };

10. var num = new Numerote(5);

11. console.log(num.numeroteTableau(['a', 'b', 'c']));// true

Mais mon code ne fonctionne pas parce que le this à l'intérieur de la fonction masque le this englobant.

Quand on tombe sur ce genre de situation on mémorise le this juste avant la fonction avec une autre variable :

1. function Numerote(depart) {

2. this.depart = depart;

3. }

4. Numerote.prototype.numeroteTableau = function (tableau) {

5. let that = this;

6. return tableau.map(function (x) {

7. return x + that.depart++;

8. });

9. };

11. var num = new Numerote(5);

12. console.log(num.numeroteTableau(['a', 'b', 'c']));  // ["a5","b6","c7"]

C'est efficace même si ce n'est pas vraiment élégant…

Une autre solution (qui fonctionne avec ECAMScript 5.1) consiste à lier this à la fonction :

return tableau.map(function (x) {
return x + this.depart++;
}.bind(this));

Avec ES6 il suffit d'utiliser une méthode fléchée :

1. Numerote.prototype.numeroteTableau = function (tableau) {

2. return tableau.map((x) => {

3. return x + this.depart++;

4. });

5. };

Là, this a la référence correcte et tout fonctionne !

III-D - En résumé

- Avec ES6 toutes les fonctions sont automatiquement nommées pour faciliter le débogage.

- Avec ES6 une fonction dans un bloc a comme portée ce bloc.

- Les fonctions fléchées simplifient la syntaxe et permettent d'avoir un this lexical.