💡

Vous êtes en train de lire le Chapitre 3 du livre “Node.js”, écrit par Thomas Parisot et publié aux Éditions Eyrolles.

L’ouvrage vous plaît ? Achetez-le sur Amazon.fr ou en librairie. Donnez quelques euros pour contribuer à sa gratuité en ligne.

Passons en revue les variables et structures ECMAScript pour mieux comprendre ce qui en fait un langage élégant et moderne.

Sommaire
  • Qu’est-ce que JavaScript ?

  • Comprendre l’évolution de la spécification ECMAScript

  • Jongler avec les différentes structures du langage

  • En savoir plus sur des éléments avancés du langage

JavaScript est souvent raillé. Parce que ce n’est pas un vrai langage. Parce qu’il a été créé en 6 jours. Parce qu’il n’est pas orienté objet.

JavaScript est un langage expressif qui a énormément gagné en maturité depuis les années 2010. Il se révèle parfaitement opérationnel dès lors que l’on s’intéresse à ses fonctionnalités, sans faire de hors-piste.

Les types de données et les méthodes de manipulations qu’elles nous offrent permettent d’écrire un code plus simple, à lire et à produire. Certaines structures de données nous aident à mieux organiser nos données ainsi qu’à mieux les traiter.

4. Qu’est-ce que JavaScript ?

Je vais vous présenter plusieurs exemples de code. Ils ont en commun d’être tous écrits en JavaScript.

intro/ecmascript.js
'use strict';

const baseUrl = 'https://apprendre-nodejs.fr/v1';
const filePath = `${baseUrl}/package.json`;

Cet exemple illustrait la création de variables, de chaînes de caractères. Ce sont des fonctionnalités de base de la spécification ECMAScript.

intro/web.js
'use strict';

fetch('https://apprendre-nodejs.fr/v1/package.json')
  .then(response => response.json())
  .then(pkg => console.log(`${pkg.name}@${pkg.version}`));

fetch() ne fait pas partie de la spécification ECMAScript. C’est un ajout des navigateurs web. On parle alors d'API JavaScript pour le Web.

💬
Glossaire API (Interface de programmation)

Les API sont des interfaces pour dialoguer avec un programme ou une ressource informatique. Elles définissent des vocabulaires pour exécuter des actions spécifiques.

intro/dom.js
'use strict';

const docBody = document.querySelector('body');
docBody.addEventListener('click', () => alert('Hello World!'));

La variable document et les méthodes querySelector et addEventListener font aussi partie des API JavaScript pour le Web. En l’occurrence, elles font partie de l’API DOM (Document Object Model), un mécanisme pour interagir avec une page web grâce à ECMAScript.

intro/node.js
'use strict';

const {homedir} = require('os');
const fs = require('fs');
fs.readdir(homedir(), (err, files) => {
  err ? console.error(err) : console.log(files);
});

Ce dernier exemple est spécifique à Node. Ce dernier propose la fonction require() pour charger des modules et interagir avec le système d’exploitation.

Autrement dit, ECMAScript est un langage, une grammaire avec des fonctionnalités de base. Chaque environnement – les navigateurs web, Node – le comprend et lui ajoute de nouvelles expressions, contextuelles à cet environnement d’exécution. JavaScript est le grand ensemble des technologies qui reposent sur ECMAScript pour fonctionner.

💬
Histoire À propos de JavaScript

JavaScript est inventé en 1995 par Brendan Eich alors qu’il est employé de la société Netscape Communications. Microsoft lui emboîte le pas en incluant JavaScript dans son logiciel Internet Explorer, alors en version 3. Pour des raisons de droits de marque, il y est dénommé JScript.

La spécification est ensuite validée par l’organisme Ecma International en juin 1997 sous le nom d’ECMAScript, standard ECMA-262.

Le terme JavaScript est resté dans le vocabulaire courant, mais, en fait, il s’agit bien d’ECMAScript.

Adobe Flash utilise un dérivé d’ECMAScript : ActionScript. Bien des machines virtuelles sont capables d’interpréter partiellement ou intégralement ECMAScript : Rhino, Konq, BESEN en Object Pascal ou encore Esprima, qui est elle-même écrite dans ce langage.

Si d’autres langages de programmation se cantonnent soit au côté client (VBScript, ActionScript, Elm), soit au côté serveur (Ruby, Python, Haskell), JavaScript a débuté côté client pour s’étendre aussi côté serveur. Un développeur ou une développeuse dite full stack programme des applications sur les deux fronts. Node a cet avantage d’unifier le langage de programmation entre les environnements client et serveur.

Le langage ECMAScript – appelons-le ainsi à partir de maintenant – a évolué au fil du temps. Il s’est enrichi de nouvelles fonctionnalités au fil des versions, mais aussi de sucres syntaxiques (raccourcis d’écriture) et de rigueur aussi, pour corriger des défauts de design.

Le comité de travail TC39 (Technical Committee, github.com/tc39) est en charge de l’évolution du langage, standardisé sous le doux sobriquet de standard ECMA-262, à charge ensuite aux différents implémenteurs de suivre les changements et de les incorporer dans leurs machines virtuelles.

Node se base sur la machine virtuelle V8 de Google pour interpréter les expressions ECMAScript. De fait, Node comprend les mêmes expressions ECMAScript que V8.

Nous verrons un peu plus tard dans ce chapitre comment suivre la compatibilité de Node avec ECMAScript. Intéressons-nous à l’évolution du langage et à ce que ça nous apporte.

4.1. ECMAScript 5 (aka ES5)

ECMAScript a été standardisé dans sa version 5 en décembre 2009. La révision 5.1 de juin 2011 est une correction mineure de la spécification.

Il s’agit d’une évolution majeure dans l’histoire du langage. La précédente version – ECMAScript 3 – était âgée de dix ans.

ECMAScript 5 limite drastiquement certains effets indésirables du langage grâce au mode strict. De nouvelles méthodes de manipulation de tableaux et d’objets voient le jour, ainsi qu’une prise en charge native du format de données JSON.

La standardisation de cette version d’ECMAScript a contribué à redorer l’image du langage, mais aussi à faire émerger de nouvelles pratiques de programmation.

compat table
Figure 1. Table de compatibilité

4.2. ECMAScript 2015 (aka ES6 puis ES2015)

La spécification ECMAScript 2015 (ES2015) a été publiée en juin 2015. Elle succède à ECMAScript 5 après six années de gestation. Cette version a successivement été appelée ECMAScript Harmony, ECMAScript 6, puis ECMAScript 2015.

De nombreuses idées ont été piochées dans le langage CoffeeScript (coffeescript.org). Et surtout, un nouveau type d’outillage s’est formé pour commencer à utiliser ce JavaScript du futur avec le compilateur traceur de Google dès 2011 (github.com/google/traceur-compiler), puis avec le projet indépendant 6to5 dès 2014. Ce dernier a été renommé en Babel (babeljs.io) et son instigateur a par la suite été embauché par Facebook.

La pratique de compiler du JavaScript en JavaScript était en rupture avec ce qui se faisait précédemment : attendre qu’une fonctionnalité soit adoptée par un dénominateur commun de navigateurs web pour s’en servir. Cette fois-ci, on pouvait se servir du futur, dès aujourd’hui.

De fait, il n’y a pas eu à attendre six ans et les différentes implémentations pour profiter de ce qu’il y avait de meilleur.

Le prix à payer ? Un ticket d’entrée plus élevé lié à la maîtrise de l’outillage associé.

Table de compatibilité (navigateurs web)

kangax.github.io/compat-table/es6/

Table de compatibilité (Node.js)

node.green/

Spécification

www.ecma-international.org/ecma-262/6.0/

node green
Figure 2. Illustration de l’évolution de la compatibilité ECMAScript au fil des versions de Node

4.3. ECMAScript 2016, etc. (aka ES2016)

Depuis la sortie d’ECMAScript 2015, l’intention est de publier une nouvelle spécification par an, de travailler les fonctionnalités une par une pour ne pas attendre trop longtemps avant de les ratifier. En conséquence, les nouvelles versions annuelles sont beaucoup plus incrémentales. Elles se font moins attendre et contiennent moins de grands bouleversements.

Les fonctionnalités en cours de préparation sont listées dans le dépôt GitHub suivant : github.com/tc39/proposals. Le dernier stade avant la validation est le stage 3. Dès qu’une fonctionnalité passe en stage 4, elle est incluse dans la prochaine version d’ECMAScript – ECMAScript 2022 une fois l’année 2022 terminée.

Les fonctionnalités approuvées sont consignées dans un document : github.com/tc39/proposals/blob/master/finished-proposals.md.

5. Éléments de base du langage

Cette section décrit les notions nécessaires pour s’approprier le reste des exemples de l’ouvrage. On apprendra notamment à créer des variables, à naviguer dans des listes d’éléments et à faire la différence entre un objet et une fonction.

5.1. Les types de données

Qu’entend-on par type de données ? Faisons-nous notre propre idée avec une suite d’exemples. Ces notions seront développées dans le reste du chapitre, pour mieux comprendre ce que l’on peut en faire.

base/string.js
'Node.js'

Une valeur entourée de guillemets est considérée par l’interpréteur ECMAScript comme une chaîne de caractères, du texte.

Ces guillemets sont selon les cas des guillemets simples ('), doubles (") ou obliques (`).

On peut effectuer des opérations d’identification ou d’assemblage avec une valeur de type chaîne de caractères.

base/number.js
3

12.3

ECMAScript considère les entiers (3 dans cet exemple) et les réels (12.3 dans cet exemple) comme des nombres. Il ne fait pas de distinction entre les deux.

On peut effectuer des opérations mathématiques entre plusieurs valeurs de type nombre.

base/boolean.js
true

false

ECMAScript considère deux valeurs pour signifier vrai ou faux : respectivement true et false.

On peut effectuer des opérations logiques avec une valeur de type booléen.

base/null.js
null

On utilise null pour signifier l'absence de valeur.

base/undefined.js
undefined

La valeur undefined est utilisée pour signifier qu’une valeur est inconnue. Rares sont les cas où on choisira ce type de données par nous-même.

📖
Documentation Primitives
Rendez-vous sur MDN web docs pour en savoir plus sur primitives.
developer.mozilla.org/docs/fr/Web/JavaScript/Data_structures

Il existe trois autres types de données qui se basent sur ces types dits primitifs. Ils sont destinés à ranger, à classer et à exprimer de nouvelles valeurs en fonction d’autres.

base/array.js
[2, 'C', 2, 'G']

Un tableau se déclare en encadrant une suite de valeurs entre crochets. Il est capable de contenir n’importe quel type de valeurs et autant que nécessaire. L’ordre des valeurs a généralement une importance.

On peut effectuer des opérations de tri et de sélection avec un tableau de valeurs.

base/object.js
{
  title: 'Node.js',
  isbn: '978-2212139938',
  published: true
}

Un objet se déclare en encadrant une suite de paires clé/valeur entre accolades. Il fonctionne comme un dictionnaire : on associe une valeur (type au choix) à une clé (un intitulé, un label). L’ordre des paires n’a généralement pas d’importance.

On peut effectuer des opérations de sélection avec un objet de valeurs.

base/function.js
function double(value) {
  return value * 2;
}

double(3);

Une fonction accepte des arguments, de n’importe quel type et autant que nécessaire. Elle doit être déclarée pour être exécutée (dernière ligne de l’exemple précédent).

Une fonction retourne un résultat explicite avec le mot-clé return. Dans le cas contraire, ECMAScript considère que la valeur retournée équivaut implicitement à undefined.

On peut effectuer des opérations de transformation avec une fonction.

Les fonctions sont destinées à être appelées, pour effectuer des traitements répétitifs. Dès que l’on doit écrire deux fois la même chose, on l’écrit dans une fonction qu’on appelle deux fois.

5.2. Les variables

Les variables servent à ranger des valeurs. On peut ainsi les réutiliser plus tard, les transmettre et prendre des décisions en fonction de ce qu’elles contiennent.

Les variables nous aident à donner du sens à notre code, à le rendre intelligible par d’autres personnes ainsi qu’à nommer des choses comme on le ferait dans notre quotidien.

base/variables.js
const book = {
  title: 'Node.js',
  isbn: '978-2212139938',
  published: true
};

const base_price = 13;

function double(value) {
  return value * 2;
}

book.price = double(base_price);

À votre avis, quel est le prix du livre calculé dans l’exemple précédent ? Il suffit de suivre le chemin que prend la nouvelle valeur rangée dans la clé price de l’objet book, calculée par la fonction double à laquelle on passe la valeur contenue dans la variable base_price.

Le mot-clé const nous a servi à déclarer une variable. On ne peut étiqueter ainsi une variable avec le même nom qu’une seule fois. L’exemple suivant générera une erreur lors de la deuxième affectation :

variables/const.js
const base_price = 13;
const base_price = 14;
💬
Question Une variable constante ?

const est un type de variable qui ne peut être ni redéclaré ni être réaffecté : c’est en ça qu’il est constant.

Une variable const n’empêche pas la modification de son contenu, dans le cas d’un tableau ou d’un objet. La fonction Object.freeze() est une réponse adaptée pour garantir son immuabilité.

variables/const-freeze.js
const table = ['a', 'b'];

table.push('c');
console.log(table);       // (1)

Object.freeze(table);     // (2)
table.push('d');          // (3)
  1. Affiche [ 'a', 'b', 'c' ].

  2. L’objet table est gelé grâce à la fonction Object.freeze().

  3. Cette ligne lance une erreur car nous tentons de modifier un objet gelé.

5.3. Les instructions

Des instructions nous servent à suivre, éviter ou répéter des chemins dans notre code.

L’instruction if exécute du code s’il remplit une condition. Cette dernière peut être une valeur ou une expression interprétée pour savoir à quel booléen elle correspond.

instructions/if.js
const book = {
  title: 'Node.js',
  published: true
};

if (book.published && book.title) {
  console.log('Le livre est publié (et a un titre)');
}

L’exemple précédent vérifie que les deux conditions sont remplies (opérateur &&) pour afficher un message en conséquence.

On notera au passage que book.title n’est pas un booléen. ECMAScript regarde dans ce cas que la chaîne de caractères contient au moins un caractère. On expliquera ce comportement plus en détail dans la section “Jongler avec des valeurs vraies ou fausses”.

L’instruction if peut être complétée avec l’instruction else pour exécuter du code qui répondrait au cas contraire. Il est possible d’imbriquer plusieurs else if à la suite.

instructions/else-if.js
const book = {
  title: 'Node.js',
  published: true
};

if (book.published && book.title) {
  console.log('Le livre est publié avec un titre.');
}
else if (book.published) {
  console.log('Le livre est publié (sans titre).');
}
else {
  console.log('Le livre n\'est pas publié.');
}

Notre exemple n’empruntera qu’un seul des chemins, mais on constate qu’on pourrait en emprunter un autre en modifiant la valeur des clés title et published.

5.4. La portée (scope)

La portée est un concept très présent dans ECMAScript. On y fait souvent référence en parlant de variable globale et de variable locale. C’est une sorte de frontière d’accès à la valeur d’une variable.

scopes/local.js
function secret(){
  const mot = 'devinette';

  return 'None shall pass';
}

console.log(secret());
console.log(mot);

Dans cet exemple, la variable secret de type fonction a une portée globale au script. En revanche, la variable mot est définie dans la fonction secret et n’est donc pas accessible en dehors de la portée de la fonction. À l’inverse, ce qui est défini en dehors d’une fonction est accessible à l’intérieur d’une fonction.

La portée de la variable mot est locale à la fonction secret.

scopes/global.js
const year = 2018;

function next(value) {
  return value + 1;
}

function nextYear() {
  return next(year);
}

console.log(year);
console.log(nextYear());
console.log(value);

Ici, nous illustrons la portée globale de la variable year. Elle est définie un cran au-dessus des fonctions next et nextYear. On peut y accéder, comme en atteste le code de la fonction nextYear.

À l’inverse, la variable value a une portée locale – elle est passée en paramètre de la fonction next. ECMAScript génèrera une erreur si on tente d’y accéder en dehors de sa portée.

La portée est délimitée par les fonctions. En l’absence de fonction, la portée maximale est celle du module (script) dans lequel la variable est déclarée.

Il existe un deuxième type de portée : la portée lexicale. L’exemple suivant servira à illustrer la nature de sa délimitation.

scopes/lexical.js
const book = {
  title: 'Node.js',
  published: true
};

if (book.published) {
  const price = 32;

  console.log(`Le livre ${book.title} coûte ${price}€.`);
}

console.log(`Le livre ${book.title} coûte ${price}€.`);

Le mot-clé const crée une variable certes, mais une variable dont la portée est lexicale. La portée lexicale est délimitée par le bloc d’instructions dans lequel la variable est déclarée.

Ainsi, la variable price n’existe que dans le cadre du bloc if.

La portée lexicale sert à déclarer des variables sans "polluer" le reste du script, pour que son existence soit oubliée aussitôt le bloc exécuté.

6. Jongler avec du texte (chaînes de caractères)

Il est commun d’avoir à travailler avec des chaînes de caractères. Elles servent à stocker des URL, des titres, des identifiants, des tweets, des messages et des textes longs, entre autres.

string/base.js
// Utilisation de guillemets simples
console.log('L\'après-midi\nLe soir');  // (1)

// Utilisation de guillemets doubles
console.log("L'après-midi\nLe soir");   // (2)

// Utilisation de guillemets obliques
console.log(`L'après-midi
Le soir`);                              // (3)
  1. Utilisation de guillemets simples (\n sert à revenir à la ligne).

  2. Utilisation de guillemets doubles : évite d’échapper le guillemet simple.

  3. Utilisation de guillemets obliques : autorise l’écriture sur plusieurs lignes.

Tous les caractères sont utilisables : lettres, chiffres, caractères accentués, émojis et même des sinogrammes ou des kanjis. Autrement dit, il n’y a pas de limite. Les environnements d’exécution se représentent les caractères au format UTF-16 (tables de stockage Unicode sur 16 bits de données).

Il est fréquent d’avoir à concaténer des chaînes de caractères, ou à les composer à partir d’une autre variable.

string/concat.js
const mot = 'pot';

console.log(`${mot} de colle`);
console.log(`${mot} de fleur`);

Toute chaîne de caractères offre un ensemble d'attributs (.quelque-chose) et de méthodes (.autre-chose()) pour en savoir plus sur la chaîne mais aussi pour la transformer.

Par exemple, on connaît la longueur d’une chaîne via son attribut length.

string/length.js
console.log('I ♥ JavaScript'.length); // (1)
console.log(''.length);               // (2)
  1. Affiche 14.

  2. Affiche 0.

On accède à un caractère spécifique en utilisant la chaîne comme un tableau, ou à l’aide d’une méthode dédiée :

string/char-at.js
const mot = 'Node.js';

console.log(mot[0]);        // (1)
console.log(mot.charAt(1)); // (2)
  1. Affiche N.

  2. Affiche o.

🚨
Attention

Le premier caractère d’une chaîne est à l’index 0 et non pas à 1.

Deux autres fonctions transforment un texte en lettres minuscules ou majuscules :

string/lower-upper-case.js
const mot = 'Node.js';

console.log(mot.toLocaleLowerCase()); // (1)
console.log(mot.toLocaleUpperCase()); // (2)
  1. Affiche node.js.

  2. Affiche NODE.JS.

D’autres fonctions nettoient ou complètent les espaces autour, au début ou à la fin d’une chaîne de caractères :

string/trim-pad.js
const mot = '  Node.js  ';

console.log(mot.trim());      // (1)
console.log(mot.trimLeft());  // (2)
console.log(mot.trimRight()); // (3)

const swiftCode = 'BARCGB22';

console.log(swiftCode.padEnd(11, 'X')); // (4)
  1. Affiche Node.js.

  2. Affiche Node.js  .

  3. Affiche   Node.js.

  4. Affiche BARCGB22XXX.

Dans cet exemple, la méthode padEnd complète jusqu’à 11 caractères, avec la lettre X. La méthode padStart fait la même chose mais avec le début de la chaîne.

indexOf retourne la position de la première occurrence dans une chaîne d’une sous-chaîne passée en paramètre. Si la valeur n’est pas trouvée, la méthode renvoie la valeur -1. À l’inverse, lastIndexOf retournera la dernière occurrence trouvée :

string/index-of.js
console.log('I ♥ JavaScript'.indexOf('JavaScript')); // (1)
console.log('I ♥ JavaScript'.indexOf('?'));          // (2)
console.log('I ♥ JavaScript'.indexOf('a'));          // (3)
console.log('I ♥ JavaScript'.lastIndexOf('a'));      // (4)
  1. Retourne 4.

  2. Retourne -1 – aucune occurrence n’a été trouvée.

  3. Retourne 5 – première occurrence de la lettre a.

  4. Retourne 7 – dernière occurrence de la lettre a.

6.1. Expressions régulières (RegExp)

Si indexOf et lastIndexOf identifient des caractères exacts, comment faire lorsque l’on souhaite chercher de manière approximative, plusieurs fois et selon certaines conditions ?

Les expressions régulières (RegExp, pour Regular Expressions) entrent en jeu dans ces cas plus avancés. Leur mécanisme décrit des motifs à identifier. Plusieurs méthodes servent ensuite à tester, identifier et remplacer ces motifs au sein d’une chaîne de caractères.

💡
Anecdote RegExp et Perl

La syntaxe d’expressions régulières est inspirée de celle du langage de programmation Perl (www.perl.org) dans sa version 5.

Une expression régulière est décrite le plus souvent en tant que motif encadré par des barres obliques, suffixé d'options exprimées sous forme de lettres :

regexp/base.js
/[a-z]+.js/i

Cet exemple utilise l’option i mais il en existe plusieurs :

Insensible à la casse (i)

On souhaite identifier du contenu, peu importe s’il est en majuscules ou non.

Multiligne (m)

La recherche s’effectue sur toutes les lignes.

Global (g)

La recherche identifie tous les résultats – au lieu du seul premier.

Unicode (u)

S’utilise si le motif de recherche exprime des séquences de caractères Unicode sous la forme \u{…​} (voir les classes de caractères ci-après).

Illustrons leur utilisation en identifiant du texte répondant (match) à une expression régulière (/…​/) :

regexp/flags.js
const text = 'I ♥ Node.js & Anode';

console.log(text.match(/node/i));   // (1)
console.log(text.match(/ode/g));    // (2)
console.log(text.match(/node/ig));  // (3)
  1. Identifie et affiche Node, l’occurrence contenue dans le mot Node.js.

  2. Affiche deux fois ode – les occurrences contenues dans les mots Node.js et Anode.

  3. Affiche Node et node en combinant les deux options i et g – les occurrences contenues dans les mots Node.js et Anode.

Des éléments de syntaxe complètent les options pour identifier des motifs au sein de chaînes de caractères :

Ensemble de caractères (entre [ et ])

Liste l’ensemble des caractères recherchés. Le caractère - indique une plage de caractères. (ex. [a-d] correspond à [abcd], donc a ou _b_ ou _c_ ou _d_).

Nombre de caractères (entre { et })

Répète un caractère ou une sous-chaîne ; exactement ({2} – exactement deux fois), au moins ({2,} – au moins deux fois) ou entre ({1,2} – entre une et deux fois).

Nombre de caractères (?+ et *)

Version raccourcie du nombre de caractères pour des besoins usuels : 0 ou 1 caractère avec ?, 1 caractère et plus avec + et 0 caractère et plus avec *.

regexp/syntax.js
const paris15 = '75015 Paris';
const avray = '92410 Ville-d\'Avray';

// test du code postal uniquement
console.log(paris15.match(/[0-9]{2}/));          // (1)
console.log(paris15.match(/[0-9]{2,5}/));        // (2)

// test du code postal et de la ville
console.log(paris15.match(/[0-9]{5} [a-zA-Z]+/)); // (3)
console.log(paris15.match(/[0-9]{5} [a-z]+/i));   // (4)

// test sur un nom de ville composé
console.log(avray.match(/[0-9]{5} [a-z]+/i));     // (5)
console.log(avray.match(/[0-9]{5} [a-z'-]+/i));   // (6)
  1. Affiche ["75"] – les 2 premiers caractères numériques de la chaîne.

  2. Affiche ["75015"] – les 5 premiers caractères numériques (satisfait la condition 5 de {2,5}).

  3. Affiche ["75015 Paris"].

  4. Affiche ["75015 Paris"] – l’option i évite de préciser l’ensemble A-Z.

  5. Affiche ["92410 Ville"] – capture les caractères jusqu’à ce que la condition ne soit plus remplie en rencontrant le trait d’union (-).

  6. Affiche ["92410 Ville-d’Avray"].

D’autres opérateurs délimitent notre recherche :

Début et fin de chaîne (^ et $)

Quand l’option multiligne (m) est utilisée, les notions de début et de fin s’appliquent au niveau de la ligne.

Limite de mot (\b)

Symbolise tout caractère ne faisant pas partie d’un mot, y compris le début ou la fin d’une chaîne.

Ou (|)

Sépare deux choix (ex. /noir|blanc/).

Groupe de capture (entre ( et ))

Délimite un groupe de caractères. Les groupes peuvent par la suite être identifiés et remplacés.
On notera également que l’emploi des groupes change la structure des résultats en un tableau de plusieurs éléments, de la forme ["chaîne identifiée", "groupe 1", "groupe 2" …​].

regexp/limits.js
const postcode = '75015 Paris';
const cedex = `CODEPOSTAL VILLE CEDEX
33900 Bordeaux Cedex 9
33074 BORDEAUX CEDEX
33700 MERIGNAC Cidex 40`;

const nogroup = /^[0-9]{5} [a-z0-9' -]+/i;
const group = /^([0-9]{5}) [a-z0-9' -]+/i;

// avec ou sans groupe de capture
console.log(postcode.match(nogroup));         // (1)
console.log(postcode.match(group));           // (2)

// mode multiligne avec ou sans option globale
console.log(cedex.match(/^([0-9]{5})/im));    // (3)
console.log(cedex.match(/^([0-9]{5})/gim));   // (4)
  1. Affiche ["75015 Paris"].

  2. Affiche ["75015 Paris", "75015"] – le premier élément correspond à la chaîne identifiée tandis que le second correspond au premier groupe de capture.

  3. Affiche ["33900", "33900"] – l’option multiligne itère de ligne en ligne jusqu’à trouver un motif.

  4. Affiche ["33900", "33074", "33700"] – l’option multiligne globale retourne tous les groupes de capture.

On notera qu’il faut faire attention à ce que l’on regarde : le format de résultat varie selon qu’on utilise ou non des groupes de capture et selon qu’on utilise l’option globale ou multiligne.

Des symboles servent de raccourcis pour désigner plusieurs caractères simultanément :

Tout caractère (.)

tout caractère sauf le saut de ligne.

Caractère de mot (\w)

Tout caractère pouvant composer un mot anglais : les caractères accentués ne sont pas englobés (identique à [A-Za-z0-9_]).

Caractère numérique (\d)

Identique à [0-9].

Caractère d’espacement (\s)

Tout caractère d’espacement : espace, tabulation, retour chariot, etc.

Caractère Unicode (\u{…​})

Doit être combiné avec l’option u (/…​/u). Exemple : ♥︎ → \u{2665}.

Les alternatives de classes en majuscules sont des négations. \W pour "tout sauf un caractère de mot", \S pour "tout sauf un caractère d’espacement", etc.

regexp/classes.js
const text = 'I ♥ RegExp in 2018';
console.log(text.match(/\u{2665} (\w+)/u));   // (1)

const [,iLove,year] = text.match(/^(I \u{2665}).+(\d{4})$/u);
console.log(`${iLove} ${year}`);              // (2)
  1. Affiche ["♥ RegExp", "RegExp"] – et s’arrête là car l’espace suivant n’est pas un caractère de mot.

  2. Affiche "I ♥ 2022" – on a extrait le début de la phrase et l’année placée en fin de chaîne.

📖
Documentation Expressions régulières
Rendez-vous sur MDN web docs pour en savoir plus sur les expressions régulières.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Global_Objects/RegExp

La méthode test est pratique si la seule chose qui vous intéresse est de tester si une chaîne correspond à un motif :

regexp/test.js
const cedex = `CODEPOSTAL VILLE CEDEX
33900 Bordeaux Cedex 9
33074 BORDEAUX CEDEX
33700 MERIGNAC Cidex 40`;

const postcode_tester = /^\d{5}\s/m;

if (postcode_tester.test(cedex)) {
  console.log(cedex.match(/^(\d{5})\b/gm));
}

Enfin, la méthode replace est très utile pour transformer des chaînes de caractères, surtout en combinaison avec les groupes de capture :

regexp/replace.js
const t = 'I ♥ JavaScript';

console.log(t.replace('♥', 'love'));    // (1)
console.log(t.replace(/\b\w+$/, 'PHP'));// (2)

const text = t.replace(/^.+(\u{2665}) (\w+)$/u, '$2 $1 me');
console.log(text);                      // (3)
  1. Affiche "I love JavaScript" – si le premier argument de replace est une chaîne, elle est convertie automatiquement en expression régulière.

  2. Affiche "I ♥ PHP".

  3. Affiche "JavaScript ♥ me" – les symboles $<numéro> représentent les groupes de capture, qu’on place dans l’ordre de notre choix.

Le second argument accepte une fonction pour procéder à des remplacements dynamiques :

regexp/replace-function.js
const text = 'I ♥ JavaScript';

const shout = text.replace(/\b(\w+)$/u, (pattern, lang) => {
  return lang.toLocaleUpperCase();
});

console.log(shout);   // (1)
  1. Affiche "I ♥ JAVASCRIPT" – le dernier mot est transformé en majuscules.

7. Jongler avec des valeurs vraies ou fausses (booléens)

Un booléen est un élément logique dont la valeur est soit true soit false, c’est-à-dire respectivement vrai ou faux. Ce type de valeur sert à exprimer des résultats de condition ("si ça alors … sinon") ainsi qu’à affirmer ou infirmer quelque chose.

boolean/base.js
const counter = 3;

console.log(counter);               // (1)
console.log(counter === 3);         // (2)

const check = (counter === 3);

console.log(check);                 // (3)
console.log(check === true);        // (4)
  1. Affiche 3.

  2. Affiche true – la condition est vérifiée.

  3. Affiche true – c’est la valeur de la variable check suite à son affectation à la ligne précédente.

  4. Affiche true.

Une donnée d’un autre type peut être convertie en booléen. La logique qui déterminera si la conversion retournera true ou false est la suivante :

true

Toute valeur non nulle.

false

Toute valeur nulle (null, 0NaN), vide ('') ou indéfinie (undefined).

boolean/convert.js
const counter = 3;

console.log(Boolean(''));       // (1)
console.log(Boolean(counter));  // (2)
console.log(Boolean([]));       // (3)

if (counter) {
  console.log('if (counter) équivaut à if (Boolean(counter))');
}
  1. Affiche false – il s’agit d’une chaîne vide.

  2. Affiche true – il s’agit d’une valeur non nulle.

  3. Affiche true – le tableau est vide mais l’objet en lui-même vaut quelque chose : un tableau.

8. Jongler avec des valeurs numériques (Number, Math)

ECMAScript ne fait pas de distinction entre des entiers et des nombres contenant des décimales : ce sont des nombres un point c’est tout.

number/base.js
console.log(40);
console.log(40.0);

console.log(40 === 40.0);   // (1)
console.log(40 === '40.0'); // (2)
  1. Affiche true – les deux valeurs sont strictement équivalentes.

  2. Affiche false – un élément entre guillemets est une chaîne de caractères, pas un nombre.

Les nombres sont représentés par défaut en base 10. La plage de nombres utilisable dans un programme est définie par des constantes ECMAScript :

number/constants.js
console.log(Number.POSITIVE_INFINITY);  // (1)
console.log(Number.NEGATIVE_INFINITY);  // (2)

console.log(Number.MAX_VALUE);          // (3)
console.log(Number.MIN_VALUE);          // (4)

console.log(Number.MAX_SAFE_INTEGER);   // (5)
console.log(Number.MIN_SAFE_INTEGER);   // (6)
  1. Affiche Infinity.

  2. Affiche -Infinity.

  3. Affiche 1.7976931348623157e+308 – le plus grand réel utilisable.

  4. Affiche 5e-324 – le plus petit réel utilisable.

  5. Affiche 9007199254740991 – le plus grand entier utilisable.

  6. Affiche -9007199254740991 – le plus petit entier utilisable.

Il est aussi possible de compter dans d’autres bases, notamment en hexadécimal (base 16). Cette dernière est exprimée en préfixant la valeur par 0x et avec les caractères de 0 à F – 0 à 9 puis A (vaut 10), B (vaut 11), etc.

number/hexa.js
console.log(0x0000);  // (1)
console.log(0x000A);  // (2)
console.log(0x00A0);  // (3)
console.log(0x0A00);  // (4)
  1. Affiche 0.

  2. Affiche 10 – car A en hexadécimal vaut 10 en décimal.

  3. Affiche 160 – pour 10×16 (une “dizaine” vaut 16).

  4. Affiche 2560 – pour 10×16×16 (une “centaine” vaut 16×16).

💬
Rumeur JavaScript est nul en virgule flottante !

ECMAScript est souvent décrié pour son incapacité à gérer les opérations mathématiques avec précision.

0.2 + 0.6

ECMAScript respecte le standard IEEE 754 de gestion de nombres à virgule flottante sur 64 bits de données. Qui d’autre l’utilise ? D’autres langages "inconnus" comme Python, PHP et Ruby, entre autres.

Pour en savoir plus : fr.wikipedia.org/wiki/IEEE_754.

8.1. Opérations mathématiques

Les nombres s’utilisent pour effectuer des opérations mathématiques. Chaque opération est dotée d’un symbole :

Opération Symbole

addition

+

soustraction

-

multiplication

*

division

/

modulo (reste de division)

%

exposant (puissance)

**

number/operations.js
console.log(2 + 4);     // (1)
console.log(2 - 4);     // (2)
console.log(2 * 4);     // (3)
console.log(2 / 4);     // (4)
console.log(2 % 4);     // (5)
console.log(2 ** 4);    // (6)
  1. Affiche 6.

  2. Affiche -2.

  3. Affiche 8.

  4. Affiche 0.5.

  5. Affiche 2.

  6. Affiche 16.

8.2. Les nombres qui n’en sont pas (NaN)

⚠️
Attention Opérations exotiques

Est-ce que vous avez déjà tenté d’additionner un nombre avec un tableau ? Pas forcément, mais ECMAScript ne vous en empêchera pas.

number/operations-types.js
console.log(10 / '1');    // (1)
console.log(10 / 0);      // (2)
console.log(10 / []);     // (3)
console.log(10 / {});     // (4)
console.log(10 + null);   // (5)
console.log(10 + true);   // (6)
  1. La chaîne '1' sera convertie en nombre (voir plus loin).

  2. On nous a toujours interdit la division par zéro ; ici, on affiche Infinity.

  3. Affiche aussi Infinity.

  4. Seule cette opération retourne NaN.

  5. Affiche 10.

  6. Affiche 11 — la valeur true est implicitement convertie en un entier.

Certaines opérations n’aboutiront pas mais n’afficheront pas d’erreur pour autant. Dans ce cas, leur résultat vaudra NaN pour not a number (littéralement : "n’est pas un nombre").

number/nan.js
console.log(10 / 'fromage');

La fonction Number.isNaN() nous aidera à vérifier si la valeur d’une variable ou le résultat d’une opération est un NaN ou non. Cette fonction retourne un booléen.

number/is-nan.js
console.log(Number.isNaN(NaN));             // (1)
console.log(Number.isNaN(10 / 'fromage'));  // (2)

console.log(Number.isNaN(10));              // (3)
console.log(Number.isNaN('fromage'));       // (4)
  1. Affiche true.

  2. Affiche true.

  3. Affiche false.

  4. Affiche false.

🚨
Assertion NaN n’est pas un nombre ?

Il faut se méfier de NaN comme de la peste, car il est considéré comme un nombre du point de vue d’ECMAScript. Toute opération mathématique impliquant NaN renverra un NaN :

number/nan-number.js
console.log(typeof NaN);  // (1)
console.log(typeof 10);   // (2)
  1. Affiche 'number'.

  2. Affiche 'number'.

Il vaut mieux s’assurer qu’une variable est à la fois un nombre et qu’elle ne vaut pas NaN :

number/is-not-a-nan.js
const result = 10 / 'fromage';

if (typeof result === 'number' && !Number.isNaN(result)) {
  console.log('Ceci est un nombre.');
}
else {
  console.log('result n\'est pas un nombre.');
}

8.3. Convertir en nombre

indexterm[nombre, conversion]

Les lignes qui précédent l’évoquent un peu : on peut passer d’autres types de données à des nombres. Idéalement, on voudra transformer explicitement quelque chose en un nombre.

Pour cela nous disposons de deux fonctions :

  • parseInt essaie d’interpréter un nombre entier.

  • parseFloat essaie d’interpréter un nombre à virgule. La fonction s’arrête dès qu’elle n’a plus affaire à un chiffre.

number/parse.js
console.log(parseInt('3.141592653589793'));   // (1)
console.log(parseFloat('3.141592653589793')); // (2)

console.log(parseInt('14.10-patch.2'));   // (3)
console.log(parseFloat('14.10-patch.2')); // (4)
  1. Affiche 3.

  2. Affiche 3.141592653589793.

  3. Affiche 14 – ça ne change rien pour parseInt.

  4. Affiche 14.1 – la fonction s’arrête à la décimale précédant une lettre.

parseInt a cette particularité que l’on peut choisir la base de la conversion avec le second argument de la fonction.

number/parse-int.js
console.log(parseInt(10, 16));    // (1)
console.log(parseInt('A', 16));   // (2)
console.log(parseInt('A00', 16)); // (3)
  1. Affiche 16.

  2. Affiche 10 – A vaut 10 en hexadécimal.

  3. Affiche 2560 – aurait pu s’écrire 0xA00.

8.4. Formater et arrondir des nombres

Si l’envie vous prenait de vouloir arrondir des nombres, il existe quelques fonctions pour vous aider :

Math.round()

Arrondit à l’entier le plus proche.

Math.ceil()

Arrondit à l’entier supérieur du nombre donné.

Math.floor()

Arrondit à l’entier inférieur du nombre donné.

number/round.js
console.log(Math.round(3.1));    // (1)
console.log(Math.round(3.8));    // (2)
console.log(Math.round(3.5));    // (3)

console.log(Math.ceil(3.14));    // (4)
console.log(Math.floor(3.99));   // (5)
  1. Affiche 3.

  2. Affiche 4.

  3. Affiche 4.

  4. Affiche 4.

  5. Affiche 3.

Enfin, on peut préserver le formatage du nombre de décimales après la virgule en transformant le nombre en chaîne de caractères grâce à la méthode toFixed() :

number/to-fixed.js
console.log(10.0101.toFixed(2));     // (1)
console.log(10.0101.toFixed(0));     // (2)
  1. Affiche '10.01'.

  2. Affiche '10'.

9. Créer et réutiliser des blocs de code (fonctions)

Une fonction est un bloc de code réutilisable et paramétrable. Elle retourne un résultat dont la valeur se calcule en fonction des paramètres que nous lui passons.

Cela se passe en deux temps :

  1. la création de la fonction ;

  2. l'exécution.

ECMAScript fournit un ensemble de fonctions de base : console.log(), setTimeout(), etc. Node ajoute les siennes (comme require()). Nous avons la liberté d’en créer nous-mêmes, spécifiques à nos besoins.

functions/base.js
const hello = (mot) => `Hello ${mot}`;  // (1)

console.log(hello);                     // (2)
console.log(hello('World'));            // (3)
console.log(hello('toi'));

const random = () => {
  const limit = 100;

  return Math.floor(Math.random() * limit);
};

console.log(random());                  // (4)
  1. On crée la fonction hello.

  2. Affiche [Function: hello] – il s’agit de la définition de la fonction.

  3. Affiche "Hello World" – il s’agit de l'exécution de la fonction, qui retourne un résultat.

  4. Affiche un nombre aléatoire entre 0 et 100 – cette fonction est invoquée sans paramètre.

L’exemple précédent nous indique qu’une fonction se découpe en trois parties :

Les arguments

C’est la partie à gauche de la flèche (). Les arguments sont séparés par des virgules.

Le corps

C’est la partie entre accolades. Quand la fonction est sur une ligne, le résultat de l’opération est implicitement retourné. On peut dans ce cas se passer du mot-clé return.

La valeur de retour

C’est la valeur renvoyée en dehors de la fonction. Elle est définie à l’aide du mot-clé return. La valeur undefined est retournée de manière implicite lorsque ce dernier est absent.

💡
Rappel Variables et portée

Le corps d’une fonction constitue une portée : toute variable définie dans le corps d’une fonction est invisible en dehors.

9.1. Les fonctions anonymes

Les fonctions anonymes sont employées en arguments d’autres fonctions. On les dit anonymes, car elles ne sont pas consignées dans des variables. Il est fréquent de les utiliser pour itérer sur des tableaux, lors d’événements ou dans des promesses.

C’est une manière élégante d’encapsuler du code à exécuter plus tard.

functions/anonymous.js
setTimeout(() => console.log('Une seconde plus tard'), 1000);

setTimeout(() => {
  console.log('Deux secondes plus tard');     // (1)
}, 2000);

process.on('exit', () => {
  console.log('Le processus se termine');     // (2)
});
  1. Affiche "Deux secondes plus tard" deux secondes après le début du script.

  2. Affiche "Le processus se termine" quand le processus se termine, une fois que toutes les actions en attente ont été exécutées.

9.2. Les fonctions de rappel (callback)

Quand une fonction est passée en argument d’une autre fonction, on appelle cela un callback. On l’appelle plus tard (to call back) que le moment où elle est définie. Elle reçoit des paramètres qui aident à reconstruire un contexte au moment de son exécution.

functions/callback.js
const printYear = (date) => {             // (2)
  console.log(date.getUTCFullYear());     // (3)
}

setTimeout(printYear, 1000, new Date());  // (1)
// équivalent à
// setTimeout(date => printYear(date), 1000, new Date());
  1. Le troisième argument (et les suivants) de setTimeout() sont transmis en paramètres de la fonction de rappel (callback).

  2. Cette fonction est invoquée une seconde après le début du script, et reçoit en paramètre la date du moment.

  3. Affiche l’année de la date passée en argument – dans cet exemple, l’année en cours.

9.3. Paramètres du reste (rest parameters)

Les paramètres du reste sont un nombre indéfini de paramètres regroupés dans un même tableau.

functions/rest.js
const combien_de = (nom, ...params) => {
  console.log(`On a compté ${params.length} ${nom}.`);
};

combien_de('patates', 'un', 'deux', 'trois'); // (1)
  1. Affiche "On a compté 3 patates.".

10. Lister, filtrer et trier des éléments (Array)

Les tableaux (ou listes indexées) servent à lister des éléments, de tout type et dans l’ordre de notre choix. Chaque élément de tableau se voit attribuer un numéro (index) qui sert à le retrouver, en itérant à l’aide de boucles ou en ayant recours à d’autres méthodes d’identification.

array/base.js
const weekdays = [
  'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi'
];

console.log(weekdays);          // (1)
console.log(weekdays.length);   // (2)
console.log(weekdays[1]);       // (3)
console.log(weekdays[0]);
console.log(weekdays[5]);       // (4)
  1. Affiche ["lundi", "mardi", "mercredi", "jeudi", "vendredi"].

  2. Affiche 5 – soit la longueur du tableau.

  3. Affiche "mardi" – un tableau commence à l’index 0.

  4. Affiche undefined – il n’y a aucun élément défini à l’index 5.

L’exemple précédent illustre plusieurs caractéristiques des collections :

  • La numérotation débute à l’index 0.

  • La propriété length contient la longueur du tableau.

  • La valeur undefined est retournée quand on tente d’accéder à un index qui n’existe pas.

10.1. Créer des tableaux à partir d’autres valeurs

indexterm[tableau, Array.from()]

La fonction Array.from() est une manière de créer un tableau à partir de quelque chose qui ressemble à un tableau.

array/from.js
console.log(Array.from('fromage'));            // (1)

// utilisation du second argument
const uppercase = (letter) => letter.toUpperCase();
console.log(Array.from('fromage', uppercase)); // (2)
  1. Affiche ["f", "r", "o", "m", "a", "g", "e"] – chaque lettre de la chaîne.

  2. Affiche ["F", "R", "O", "M", "A", "G", "E"] – chaque lettre de la chaîne a été passée en majuscule.

Le deuxième argument de Array.from() est facultatif. C’est une fonction anonyme qui s’utilise comme les méthodes d’itération Array.forEach() et Array.map().

Cette méthode est des plus utiles pour itérer sur des listes d’éléments DOM obtenues avec les fonctions document.querySelectorAll() et document.getElementsByTagName(), entre autres.

const links = document.querySelectorAll('a');

console.log(Array.from(links).map(a => a.textContent));
// parce qu'on ne peut pas faire
// links.map(a => a.textContent);

10.2. Combiner des tableaux

Il est relativement aisé de composer des tableaux en fonction d’autres tableaux. Une première manière d’y parvenir est d’utiliser la méthode concat() :

array/concat.js
const mousquetaires = ['Athos', 'Porthos', 'Aramis'];
const extras = ['d\'Artagnan', 'Albert'];

console.log(mousquetaires.concat(extras));      // (1)

// autre manière d'obtenir la liste des 5 mousquetaires
console.log([].concat(mousquetaires, extras));
  1. Affiche ["Athos", "Porthos", "Aramis", "d’Artagnan", "Albert"].

Cette méthode crée un nouveau tableau à partir de deux passés en paramètres.

💬
Alternative Opérateur …​ (spread)

Une autre manière de faire est d’utiliser l’opérateur …​ (aussi appelé spread) pour éclater plusieurs tableaux et les rassembler dans un autre :

array/spread.js
const mousquetaires = ['Athos', 'Porthos', 'Aramis'];
const extras = ['d\'Artagnan', 'Albert'];

console.log([...mousquetaires, ...extras]);   // (1)
  1. Affiche ["Athos", "Porthos", "Aramis", "d’Artagnan", "Albert"].

À l’inverse, la méthode join() concatène tous les éléments dans une chaîne de caractères avec le séparateur de notre choix (optionnel).

array/join.js
const headers = ['ID', 'NOM', 'PRENOM'];

console.log(headers.join());    // (1)
console.log(headers.join(';')); // (2)
console.log(headers.join(''));  // (3)
  1. Affiche "ID,NOM,PRENOM" – le séparateur par défaut est une virgule.

  2. Affiche "ID;NOM;PRENOM" – on a choisi le point-virgule comme séparateur.

  3. Affiche "IDNOMPRENOM".

10.3. Itérer sur les valeurs avec des boucles

Les boucles sont une manière de parcourir plusieurs valeurs. Elles aident à mettre en place des automatismes pour éviter de répéter du code.

array/loop.js
const weekdays = [
  'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi'
];

for (const day of weekdays) {
  console.log(day);                           // (1)
}

// on obtient le même résultat avec la méthode `forEach`
weekdays.forEach((day) => console.log(day));
  1. Affiche successivement chaque valeur du tableau – "lundi", "mardi", "mercredi", "jeudi", "vendredi".

Prenons le temps de revenir sur cet exemple. On y découvre plusieurs manières d’écrire des boucles sur un tableau :

for…​of

On affecte une variable avec chaque élément (opérateur of) du tableau. Les expressions situées entre accolade sont exécutées pour chaque élément du tableau.

forEach(element ⇒ expression)

La méthode forEach applique une fonction anonyme pour chaque élément du tableau.

Il y a en réalité deux manières d’itérer avec la boucle for : sur les index (avec l’opérateur in) et sur les valeurs (avec l’opérateur of).

array/for-of-in.js
const weekend = ['samedi', 'dimanche'];

for (const index in weekend) {
  console.log(index);             // (1)
  console.log(weekend[index]);    // (2)
}

for (const day of weekend) {
  console.log(day);               // (3)
}
  1. Affiche successivement 0 puis 1.

  2. Affiche successivement "samedi" puis "dimanche" – l’index sert à retrouver la valeur dans le tableau.

  3. Affiche successivement "samedi" puis "dimanche".

La méthode forEach() propage en réalité trois arguments à notre fonction anonyme : l’élément en cours de l’itération, l’index de l’élément et le tableau d’origine.

Pourquoi passer le tableau d’origine alors qu’on itère dessus ? Pour donner du contexte au cas où on opère avec une fonction nommée. Nous verrons un usage concret de ce troisième argument dans la section “Transformer les valeurs”.

array/foreach-function.js
const undeux = ['un', 'deux'];

const printIndex = (element, index, array) => {
  console.log(`${element} : index ${index}`);  // (2)
}

undeux.forEach(printIndex);                    // (1)
  1. Applique la fonction printIndex() pour chaque élément du tableau undeux.

  2. Affiche successivement "un : index 0" puis "deux : index 1".

Outre l’inspection et l’affichage des valeurs, les boucles offrent la liberté de trier, de transformer les valeurs, de filtrer selon des conditions, mais aussi de créer de nouvelles structures de données.

Ces méthodes sont décrites dans les sections suivantes.

10.4. Trier les valeurs

La méthode sort() change l’ordre des éléments d’un tableau. Elle utilise une fonction anonyme qui compare deux éléments entre eux ; elle retourne un nombre positif, négatif ou égal à zéro selon la logique que l’on souhaite donner au tri :

  • Quand la comparaison est négative, sort() place le premier élément avant le second.

  • Quand la comparaison est positive, sort() place le premier élément après le second.

  • Quand la comparaison est égale à zéro, nulle ou non spécifiée, l’ordre des éléments reste inchangé.

array/sort.js
const sortAsc = (a, b) => a - b;
const sortDesc = (a, b) => b - a;

console.log([1, 3, 2].sort(sortAsc));                 // (1)

const undeux = [
  {label: 'deux', order: 2},
  {label: 'un', order: 1}
];

console.log(undeux.sort((a, b) => a.order - b.order));// (2)
  1. Affiche [1, 2, 3].

  2. Affiche [ { label: "un", order: 1 }, { label: "deux", order: 2 } ] – le tableau a été trié sur la valeur de order.

Les chaînes de caractères peuvent être comparées avec localeCompare(). Cette méthode retourne un nombre après une comparaison caractère par caractère entre deux chaînes.

array/sort-strings.js
const sortAlpha = (a, b) => a.localeCompare(b);
console.log(['A', 'b', 'c', 'a'].sort(sortAlpha));// (1)

const undeux = [
  {label: 'un', order: 1},
  {label: 'deux', order: 2}
];

const sortLabel = (a, b) => a.label.localeCompare(b.label);
console.log(undeux.sort(sortLabel));              // (2)
  1. Affiche ["a", "A", "b", "c"] – les majuscules influencent le tri.

  2. Affiche [ { label: "deux", order: 2 }, { label: "un", order: 1 } ] – le tableau a été trié sur la valeur de label.

💡
Alternative Array.reverse()

La méthode reverse() transforme le tableau d’origine en inversant l’ordre de ses éléments.

array/reverse.js
const weekend = ['samedi', 'dimanche'];

weekend.reverse();

console.log(weekend);     // (1)
  1. Affiche ["dimanche", "samedi"].

10.5. Transformer les valeurs

La méthode map() fonctionne quasiment comme forEach(), à ceci près qu’elle retourne un nouveau tableau, constitué des valeurs retournées par la fonction appliquée sur chaque élément.

array/map.js
const newArray = ['a', ' b', 'c '].map(value => {
  return value.trim().toUpperCase();
});

console.log(newArray); // (1)
  1. Retourne ['A', 'B', 'C'] – on a passé tous les éléments en lettres majuscules.

Le troisième argument de la méthode map() prend ici tout son sens. Par exemple, si l’on souhaite dédoublonner un tableau :

array/map-dedupe.js
const soundcheck = ['un', 'deux', 'un', 'deux'];

const dedupe = (element, index, array) => {
  if (array.slice(index+1).includes(element)) {
    return null;
  }

  return element;
}

console.log(soundcheck.map(dedupe));          // (1)
  1. Affiche [null, null, "un", "deux"].

Cet exemple vérifie, à chaque itération, si la valeur de l’élément est contenue dans la suite du tableau. array.slice(index+1) crée un nouveau tableau contenant tous les éléments situés après l’élément courant (index+1).

La méthode de transformation reduce() est différente, car elle passe le résultat de la précédente itération à la suivante. C’est comme si elle accumulait les résultats. Elle retourne une valeur finale qui peut être autre chose qu’un tableau.

array/reduce.js
const stats = [2, 4, 6, 10];

const sum = (previous, element) => previous + element;  // (2)

console.log(stats.reduce(sum, 0));    // (1)
  1. Effectue une réduction à l’aide de la fonction sum() et d’une valeur par défaut de 0 – affiche 22 à l’issue des itérations .

  2. La valeur de l’élément est le second paramètre ; le premier paramètre correspond au résultat de l’itération précédente ou à la valeur initiale, passée en argument à reduce().

10.6. Filtrer les valeurs

La méthode filter() retourne un nouveau tableau filtré à l’aide d’une fonction anonyme. Seuls les éléments qui satisfont à la condition établie par la fonction se retrouvent dans le nouveau tableau.

array/filter.js
const values = [null, 'un', 'deux', 3];

const is_finite = (value) => Number.isFinite(value);
const direct = (value) => value;

console.log(values.filter(is_finite));  // (1)
console.log(values.filter(direct));     // (2)
  1. Retourne [3] – c’est la seule valeur qui soit un nombre.

  2. Retourne ["un", "deux", 3] – ce sont les valeurs non nulles.

10.7. Identifier des valeurs

Les méthodes indexOf(), lastIndexOf() et includes() identifient une valeur exacte au sein d’un tableau.

indexOf() et lastIndexOf() retournent l’index de la valeur recherchée. Si aucun élément n’a été retrouvé, elles retourneront la valeur -1.
includes() retourne un booléen indiquant si la recherche est fructueuse (true) ou non (false).

array/index-of-includes.js
const soundcheck = ['un', 'deux', 'un', 'deux'];

console.log(soundcheck.indexOf('un'));        // (1)
console.log(soundcheck.indexOf('deux'));      // (2)
console.log(soundcheck.indexOf('trois'));     // (3)

console.log(soundcheck.lastIndexOf('deux'));  // (4)

console.log(soundcheck.includes('deux'));     // (5)
console.log(soundcheck.includes('trois'));    // (6)
  1. Affiche 0 – le premier "un" est l’élément ``0` du tableau.

  2. Affiche 1 – le premier "deux" est l’élément ``1` du tableau.

  3. Affiche -1 – cet élément est absent du tableau.

  4. Affiche 3 – le dernier "deux" est l’élément ``3` du tableau.

  5. Affiche true – l’élément "un" existe dans le tableau.

  6. Affiche false – l’élément "trois" n’existe pas dans le tableau.

Il existe ensuite d’autres méthodes comme find(), some() et every(). Elles identifient des éléments à partir d’une fonction. Les conditions de recherche sont plus complètes, car on n’est pas obligé de connaître la valeur exacte recherchée.

La méthode find() retourne le premier élément qui remplisse la condition ; findIndex() en retourne l'index.

array/find.js
const values = [null, 2, 10, 100];

const biggerThan50 = (value) => value > 50;   // (1)

console.log(values.find(biggerThan50));       // (2)
console.log(values.findIndex(biggerThan50));  // (3)
  1. La fonction retourne true si la valeur passée en argument est un nombre supérieur à 50.

  2. Affiche 100.

  3. Affiche 3 – c’est l’index de la valeur 100.

Les méthodes some() et every() retournent true respectivement si au moins une itération est satisfaisante et si toutes les itérations sont satisfaisantes.

array/some.js
const values = [1, 'a', 120, undefined, 4];
const isUndefined = (value) => value === undefined;

console.log(values.every(isUndefined));                // (1)
console.log(values.some(isUndefined));                 // (2)
console.log(values.filter(d => d).some(isUndefined));  // (3)
  1. Affiche false – toutes les valeurs ne sont pas égales à undefined.

  2. Affiche true – au moins une valeur est égale à undefined.

  3. Affiche false – il n’y a plus de valeur undefined dans le tableau, car on a utilisé la méthode filter pour supprimer les valeurs vides.

10.8. Décomposition de tableau (destructuring)

L’affectation par décomposition (destructuring) est une manière élégante de piocher des valeurs dans un tableau. Ce mécanisme n’altère pas le contenu des variables décomposées et existe aussi pour les objets.

array/destructuring.js
const weekdays = [
  'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi'
];

const [first, second] = weekdays;
console.log(first);               // (1)
console.log(second);              // (2)

const [,, third] = weekdays;
console.log(third);               // (3)
  1. Affiche "lundi".

  2. Affiche "mardi".

  3. Affiche "mercredi" – l’utilisation des virgules sans variable a permis de sauter des positions dans la décomposition.

La décomposition se combine agréablement avec l’opérateur …​ (spread). Il accumule le reste des éléments dans une variable, sous forme de tableau.

array/destructuring-rest.js
const weekdays = [
  'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi'
];

const [first, second, ...rest] = weekdays;
console.log(rest);                  // (1)

// revient au même que
// const [,, ...rest] = weekdays;
  1. Affiche ["mercredi", "jeudi", "vendredi"].

La méthode slice() offre davantage de souplesse pour gérer les limites. On choisit l’index de début (inclus) et celui de fin (non inclus) de la décomposition.

array/slice.js
const values = ['un', 'deux', 'trois', 'quatre'];

console.log(values.slice(1));     // (1)
console.log(values.slice(1, 2));  // (2)
  1. Affiche ["deux", "trois", "quatre"] – à partir de l’index 1.

  2. Affiche ["deux"] – à partir de l’index 1 et jusqu’à l’index 2 (non inclus).

Si les valeurs de début et/ou de fin sont négatives, les index sont calculés à partir de la fin du tableau.

array/slice-negative.js
const values = ['un', 'deux', 'trois', 'quatre'];

console.log(values.slice(-1));     // (1)
console.log(values.slice(-3));     // (2)

console.log(values.slice(0, -1));  // (3)
console.log(values.slice(0, -3));  // (4)
  1. Affiche ["quatre"] – premier élément à partir de la fin.

  2. Affiche ["deux", "trois", "quatre"] – les trois premiers éléments à partir de la fin.

  3. Affiche ["un", "deux", "trois"] – jusqu’au dernier élément à partir de la fin (non inclus).

  4. Affiche ["un"] – jusqu’au troisième élément à partir de la fin (non inclus).

11. Représenter des structures d’objet et y accéder

Les structures d’objet servent à lister des éléments de tout type au sein d’une même variable. L’indexation se fait comme dans un dictionnaire, avec un identifiant unique pour chaque valeur.

object/base.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
};

console.log(francine.first_name); // (1)

francine.age = 25;                // (2)
console.log(francine.age);        // (3)
console.log(francine.twitter);    // (4)
  1. Affiche "Francine".

  2. On affecte une valeur numérique à l’index age une fois l’objet créé.

  3. Affiche 25 – la valeur numérique précédemment affectée.

  4. Affiche undefined – aucune valeur n’est affectée pour cette clé.

Une autre syntaxe existe pour créer des valeurs et y accéder en utilisant des variables en guise d’identifiant d’index.

object/dynamic.js
const SOCIAL_NETWORK = 'twitter';

const francine = {
  first_name: 'Francine',
  location: 'Drôme',
  [SOCIAL_NETWORK]: '@FrancineDu26',    // (1)
};

console.log(francine[SOCIAL_NETWORK]);  // (2)

// manières équivalentes, sans utilisation de variable
console.log(francine.twitter);
console.log(francine['twitter']);
  1. Affecte la chaîne @FrancineDu26 dans l’index correspondant à la valeur de la variable SOCIAL_NETWORK.

  2. Affiche "@FrancineDu26".

11.1. Décomposition d’objet (destructuring)

L’affectation par décomposition (destructuring) est une manière élégante de piocher des valeurs dans un objet. Ce mécanisme existe aussi pour les tableaux.

object/destructuring.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
  twitter: '@FrancineDu26',
};

const {location,twitter} = francine;
console.log(location);            // (1)

const {first_name:prenom} = francine;
console.log(prenom);              // (2)

const {is_admin=false} = francine;
console.log(is_admin);            // (3)
  1. Affiche "Drôme" – on a décomposé la clé location.

  2. Affiche "Francine" – on a décomposé puis renommé la clé first_name en une nouvelle variable : prenom.

  3. Affiche false – on a décomposé la clé is_admin et, comme elle n’existe pas, on a spécifié la valeur par défaut false, au lieu de undefined.

La décomposition se combine agréablement avec l’opérateur …​ (spread). Il accumule le reste des éléments dans une variable, sous forme d’objet.

object/destructuring-rest.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
  twitter: '@FrancineDu26',
};

const {first_name, ...metadata} = francine;

console.log(first_name);          // (1)
console.log(metadata);            // (2)
  1. Affiche "Francine".

  2. Affiche { location: "Drôme", twitter: "@FrancineDu26" }.

11.2. Combiner des objets

Object.assign() est une méthode qui sert à étendre et combiner plusieurs objets. On a le choix d’intégrer les nouveaux éléments à un objet existant ou bien d’en créer un nouveau. Les objets sont combinés dans le premier paramètre de la fonction.

object/assign.js
const o = {
  first_name: 'Francine',
};

const o2 = Object.assign({}, o, {location: 'Drôme'});
console.log(o2);        // (1)
console.log(o);         // (2)

Object.assign(o2, {location: 'Paris'}, {location: 'Ardèche'});
console.log(o2);        // (3)
  1. Affiche { first_name: "Francine", location: "Drôme" } – la nouvelle variable contient nos deux objets combinés.

  2. Affiche { first_name: "Francine" } – ce sont les valeurs originelles de notre objet.

  3. Affiche { first_name: "Francine", location: 'Ardèche' } – l’objet o2 a reçu la nouvelle propriété location.

Notez que les affectations se font de gauche à droite. Toute clé existante est remplacée.

La décomposition d’objet sert également à combiner des objets entre eux.

object/destructuring-spread.js
const francine = {
  first_name: 'Francine',
};

const francine26 = {...francine, location: 'Drôme'};
console.log(francine26);      // (1)
  1. Affiche { first_name: "Francine", location: "Drôme" }.

11.3. Itérer sur des objets

La méthode Object.entries() est probablement la plus adaptée pour itérer à la fois sur les clés et sur les valeurs d’un objet. Elle retourne un tableau qui contient autant de paires de [clé, valeur] qu’il y a d’éléments dans l’objet.

object/entries.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
};

console.log(Object.entries(francine)); // (1)
  1. Affiche [[ "first_name", "Francine" ], [ "location", "Drôme" ]].

Nous sommes libres d'itérer sur les valeurs et d’utiliser la décomposition de tableaux pour rendre notre code explicite :

object/entries-loop.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
};

Object.entries(francine).forEach(([key, value]) => {
  console.log(`francine.${key} vaut ${value}`);   // (1)
})

// même résultat, autre méthode
for (const [key, value] of Object.entries(francine)) {
  console.log(`francine.${key} vaut ${value}`);
}
  1. Affiche successivement "francine.first_name vaut Francine" puis "francine.location vaut Drôme".

Deux autres méthodes récupèrent soit la liste des clés d’un objet (Object.keys()) soit la liste de ses valeurs (Object.values()). Dans les deux cas, les résultats sont retournés sous forme d’un tableau.

object/keys.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
};

console.log(Object.keys(francine));     // (1)
console.log(Object.values(francine));   // (2)
  1. Affiche ["first_name", "location"].

  2. Affiche ["Francine", "Drôme"].

11.4. Identifier des valeurs

Il y a trois manières d’identifier si un objet contient une valeur associée à une clé.

Le plus simple est d’utiliser la méthode hasOwnProperty(). Elle prend en argument le nom de la clé à tester et retourne un booléen.

object/has-own-property.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
};

console.log(francine.hasOwnProperty('location'));   // (1)
console.log(francine.hasOwnProperty('twitter'));    // (2)
  1. Affiche true.

  2. Affiche false – cette clé n’existe pas dans cet objet.

La seconde manière est d’utiliser l’opérateur in. On l’aura déjà rencontré lors des boucles ; ici, on l’utilise une seule fois.

object/key-in.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
};

console.log('location' in francine);   // (1)
console.log('twitter' in francine);    // (2)
  1. Affiche true.

  2. Affiche false – cette clé n’existe pas dans cet objet.

Enfin, on peut tester la valeur associée avec la syntaxe standard objet.clé.

object/key.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
};

console.log(Boolean(francine.location));   // (1)
console.log(Boolean(francine.twitter));    // (2)
  1. Affiche true.

  2. Affiche false.

Attention toutefois : cette méthode teste uniquement la valeur. Si la clé existe et contient undefined, vous ne verrez pas la différence.

object/key-undefined.js
const francine = {
  first_name: 'Francine',
  location: 'Drôme',
  twitter: undefined
};

console.log(Boolean(francine.twitter));          // (1)
console.log(francine.twitter !== undefined);     // (2)

console.log(francine.hasOwnProperty('twitter')); // (3)
console.log('twitter' in francine);              // (4)
  1. Affiche false – la valeur undefined est convertie en false.

  2. Affiche false – la clé existe bien, mais elle contient la valeur undefined.

  3. Affiche true – le test se fait sur l’existence de la clé.

  4. Affiche true – idem.

12. Lire et écrire des données au format JSON

JSON (json.org) est un format de données textuel standardisé. Son but est de représenter des données informatiques de manière interopérable entre différents langages.

json/base.json
{
  "title": "Node.js",
  "price_tag": 32,
  "keywords": [
    "JavaScript",
    "Node.js",
    "Apprendre par l'exemple"
  ]
}

Le format JSON ressemble beaucoup à une structure d'objet ECMAScript. La représentation est plus stricte car toute donnée doit être représentée de manière textuelle. Ainsi, toutes les clés sont entourées de guillemets doubles.

Les types de données autorisés sont les nombres, les chaînes de caractères, les booléens, les tableaux, les objets et la valeur null. On ne peut donc pas représenter de fonction, d'instance d’objet ni même la valeur undefined.

ECMAScript embarque le nécessaire pour parser depuis et convertir en JSON. Cela se fait respectivement avec les fonctions JSON.parse() et JSON.stringify().

La fonction JSON.parse() consomme du texte. Elle retourne une représentation ECMAScript ou lance une erreur en cas de problème.

json/parse.js
const json_object = '{ "title": "Node.js", "price_tag": 32 }';
const json_string = '"Hello World!"';
const json_number = '32';

console.log(JSON.parse(json_string)); // (1)
console.log(JSON.parse(json_number)); // (2)
console.log(JSON.parse(json_object)); // (3)
  1. Affiche "Hello World!".

  2. Affiche 32.

  3. Affiche {price_tag: 32, title: "Node.js"}.

À l’inverse, la fonction JSON.stringify() convertit une structure ECMAScript en chaîne de caractères au format JSON :

json/stringify.js
const location = {
  lat: 48.8503439,
  lon: 2.34658949
}

console.log(JSON.stringify(location));  // (1)
  1. Affiche "{\"lat\":48.8503439,\"lon\":2.34658949}".

La fonction JSON.stringify() parcourt tous les éléments pour les sérialiser en forme textuelle. Quand elle rencontre la la clé spéciale toJSON(), elle l’utilise pour effectuer la conversion :

json/to-json.js
const location = {
  lat: 48.8503439,
  lon: 2.34658949,
  toJSON() {
    return `geo=${this.lat},${this.lon}`;
  }
}

console.log(JSON.stringify(location));    // (1)

delete location.toJSON;
console.log(JSON.stringify(location));    // (2)
  1. Affiche "\"geo=48.8503439,2.34658949\"" – c’est la sérialisation définie par notre fonction toJSON.

  2. Affiche "{\"lat\":48.8503439,\"lon\":2.34658949}" – sans la clé toJSON, notre objet initial est sérialisé tel quel.

Notre implémentation contenue dans la fonction toJSON() est responsable de renvoyer du texte seulement et de choisir les clés à sérialiser.

json/to-json-extra.js
const location = {
  lat: 48.8503439,
  lon: 2.34658949,
  city: 'Paris',
  toJSON() {
    return `geo=${this.lat},${this.lon}`;
  }
}

console.log(JSON.stringify(location));    // (1)
  1. Affiche "\"geo=48.8503439,2.34658949\"".

Dans cette variante d’exemple, la clé city n’a pas été sérialisée car notre fonction toJSON() se préoccupait seulement des clés lat et `lon`.

📖
Documentation JSON
Rendez-vous sur MDN web docs pour en savoir plus sur JSON.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Global_Objects/JSON

13. Interagir avec des dates

Les calculs de date s’effectuent à l’aide des objets Date. Chaque instance représente un moment dans le temps, à un jour et à une heure donnée.

date/base.js
const now = new Date();
const past = new Date('2013-12-04 10:00:00'); // (1)

console.log(past.getUTCFullYear());           // (2)
console.log(now.getUTCFullYear());            // (3)
  1. On initialise l’objet date past au 04 décembre 2013.

  2. Affiche 2013 – l’année liée à l’objet past.

  3. Affiche 2022 – l’année liée à l’objet now (date du jour).

Un certain nombre de méthodes retournent différents éléments de la date contenue dans l’objet : année, secondes, jour de la semaine, etc. Il en existe tout autant pour modifier ces éléments de date.

date/set.js
const past = new Date('2013-12-04 10:00:00');

past.setUTCFullYear('2015');           // (1)
console.log(past.toISOString());       // (2)

past.setUTCMonth(1);                   // (3)
console.log(past.toISOString());       // (4)
  1. Change la date vers l’année 2015.

  2. Affiche "2015-12-04T10:00:00.000Z".

  3. Change la date vers le mois 1.

  4. Affiche "2015-02-04T10:00:00.000Z" – pourquoi le mois de février ??

L’exemple précédent illustre l’ambiguïté de la notion de mois. Il s’agit en réalité de l'index du mois : 0 correspond à janvier, 1 à février, etc.

Les méthodes natives font pour la plupart référence à l’anglais. Elles offrent peu de confort de manipulation – on aimerait pouvoir compter facilement le nombre de jours entre deux dates, ou retirer 30 jours.

Quand nous utiliserons Node et npm, nous verrons que nous aurons à disposition des bibliothèques facilitant les manipulations de dates.

📖
Documentation Date
Rendez-vous sur MDN web docs pour en savoir plus sur Date.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Global_Objects/Date

13.1. Formatage internationalisé (Intl.DateTimeFormat)

La spécification ECMA Intl a été conçue pour ajouter des fonctionnalités relatives aux langues. Cette spécification est complémentaire. Son comportement varie en fonction du système d’exploitation – mode d’installation de Node et/ou version du navigateur web.

Les méthodes toLocaleString(), toLocaleDateString() et toLocaleTimeString() renvoient respectivement une version localisée d’une date complète, d’une date et d’une heure.

date/to-locale-date.js
const past = new Date('2013-12-04 10:00:00');

console.log(past.toLocaleDateString());                 // (1)

const options = { month: 'long' };
console.log(past.toLocaleDateString('fr-FR', options)); // (2)
  1. Affiche 04/12/2013.

  2. Affiche décembre.

⚠️
Attention M01, M02, etc. ?

Si, en formatant une date, les caractères M01, M02 ou autre s’affichent, c’est que le système n’est pas configuré avec les libellés de la langue demandée.

La langue par défaut est l’anglais.

📖
Documentation Date/toLocaleDateString
Rendez-vous sur MDN web docs pour en savoir plus sur Date/toLocaleDateString.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString

Une version plus verbeuse consiste à créer un formateur avec Intl.DateTimeFormat. Ce formateur se réutilise pour transformer plusieurs fois des dates différentes avec les mêmes réglages ou une même date avec des formatages différents.

date/intl.js
const past = new Date('2013-12-04 10:00:00');
const options = {
  year: 'numeric', month: 'short', day: 'numeric'
};
const longOptions = {
  year: 'numeric', month: 'long', day: 'numeric',
  weekday: 'long'
};

const fr = new Intl.DateTimeFormat('fr-FR', options);
const frLong = new Intl.DateTimeFormat('fr-FR', longOptions);

console.log(fr.format(past));         // (1)
console.log(frLong.format(past));     // (2)
  1. Affiche 4 déc. 2013.

  2. Affiche mercredi 4 décembre 2013.

📖
Documentation DateTimeFormat
Rendez-vous sur MDN web docs pour en savoir plus sur DateTimeFormat.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Global_Objects/DateTimeFormat

14. Partager une logique avec des objets de même nature (Class)

Une classe est une structure qui partage des propriétés et des méthodes entre les objets qui y font appel. Une instance de classe est créée en préfixant un appel de fonction par l’opérateur `new`.

const date1 = new Date();
const date2 = new Date('2013-12-04');

Nos deux variables sont des objets issus de la classe Date. Chacune des variables bénéficie des méthodes définies par cette classe.

Autrement dit, si les structures d’objet définissent des données, les classes définissent des comportements partagés.

class/base.js
class Book {
  constructor({ title, ean13 }) {     // (1)
    this.title = title;               // (2)
    this.ean13 = ean13;
  }

  toJSON() {                          // (3)
    const {title, ean13} = this;
    return {title, ean13};
  }

  get isbn() {                        // (4)
    return this.ean13.split(3)[1];
  }

  static clean(value) {               // (5)
    return value.replace(/\D/g, '');
  }
}
  1. Le constructeur reçoit un ou plusieurs argument(s) lors de l’instanciation de la classe.

  2. this fait référence à ce contexte, c’est-à-dire à cette instance de classe ; deux instances peuvent être initialisées avec des données différentes.

  3. toJSON() est une méthode de la classe.

  4. isbn() est un accesseur (préfixe get) – une propriété dont la valeur est calculée à chaque fois qu’elle est appelée.

  5. clean() est une méthode dite statique – elle est appelée en dehors d’une instance.

Nous développerons cet exemple dans les sections qui suivent. On peut d’ores et déjà noter que la structure d’une classe se décompose en plusieurs parties :

La définition

Définit le nom de la classe que l’on pourra instancier.

Le constructeur

Partie exécutée lorsque la classe est instanciée. On y met le moins de choses possibles. En général, on copie les données passées en argument.

Les méthodes

Fonctions partagées entre toutes les instances de la classe.

Les méthodes statiques

Fonctions partagées sans avoir à instancier la classe.

Les accesseurs et mutateurs

Fonctions qui définissent le comportement de propriétés dynamiques.

Le contexte (this)

On peut s’y référer dans les méthodes de la classe pour dire je fais référence à cet objet et, donc, appeler les données et méthodes attenantes.

📖
Documentation Classes
Rendez-vous sur MDN web docs pour en savoir plus sur les classes.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Classes

14.1. Méthodes d’instance

Les méthodes définissent des comportements partagés entre chaque instance de la classe. Elles servent à retourner ou transformer des valeurs rattachées à l’objet.

class/methods.js
class Book {
  constructor({ title }) {
    this.title = title;
  }

  isPublished() {
    return this.is_published === true;
  }

  publish() {
    this.is_published = true;
  }
}

const book1 = new Book({ title: 'Node.js' });
console.log(book1.isPublished());   // (1)
book1.publish();
console.log(book1.isPublished());   // (2)

const book2 = new Book({ title: 'CSS maintenables' });
console.log(book2.isPublished());   // (3)
  1. Affiche false – la propriété n’existe pas.

  2. Affiche true – la propriété is_published a été changée à la ligne précédente.

  3. Affiche false – les données sont étanches entre chaque instance.

14.2. Méthodes statiques

Les méthodes statiques sont pratiques pour mettre à disposition du code métier de manière organisée. Elles se caractérisent par le mot-clé static devant un nom de fonction.

class/static.js
class Book {
  constructor({ title, ean13 }) {
    this.title = title;
    this.ean13 = ean13;
  }

  static clean(value) {
    return value.replace(/\D/g, '');
  }
}

const nodebook = new Book({
  title: 'Node.js',
  ean13: Book.clean('978-2212139938'),  // (1)
});

console.log(nodebook.ean13);            // (2)
console.log(nodebook.clean);            // (3)
  1. On appelle la méthode statique Book.clean() pour nettoyer le code EAN13.

  2. Affiche "9782212139938" – la valeur a bien été nettoyée.

  3. Affiche undefined – les méthodes statiques ne sont pas accessibles depuis l’instance de classe.

On verra dans le chapitre sur Node qu’on peut se baser sur les modules pour partager du code sans avoir à l’affecter à une classe.

📖
Documentation Méthodes statiques
Rendez-vous sur MDN web docs pour en savoir plus sur les méthodes statiques.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Classes/static

14.3. Accesseurs et mutateurs

Ce type de méthode définit des attributs dont la lecture ou l’écriture sont dynamiques.

L’accesseur est une fonction préfixée par le mot-clé get ; elle retourne la valeur d’un attribut.

getters.js
const book = {
  title: 'Node.js',
  ean13: '9782212139938',
  get isbn() {            // (1)
    return this.ean13.slice(3);
  }
}

console.log(book.ean13);  // (2)
console.log(book.isbn);   // (3)
  1. Définition de l’accesseur isbn().

  2. Affiche "9782212139938" – c’est une propriété de l’objet nodebook.

  3. Affiche "2212139938" – isbn() s’utilise comme un attribut mais sa valeur est calculée à chaque fois qu’elle est appelée.

Le mutateur est une fonction préfixée par le mot-clé set ; elle définit la valeur d’un ou plusieurs attribut(s).

setters.js
const book = {
  title: 'Node.js',
  set ean13 (value) {     // (1)
    this.issn = value.slice(0, 3);
    this.isbn = value.slice(3);
  }
}

book.ean13 = '9782212139938';

console.log(book.issn);   // (2)
console.log(book.isbn);   // (3)
console.log(book.ean13);  // (4)
  1. Définition du mutateur ean13() – il accepte un seul argument.

  2. Affiche 978 – l’attribut a été créé lors de l’appel du mutateur.

  3. Affiche 2212139938 – idem.

  4. Affiche undefined – il faudrait créer un accesseur get ean13() pour recomposer dynamiquement sa valeur.

📖
Documentation Accesseurs
Rendez-vous sur MDN web docs pour en savoir plus sur les accesseurs.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Functions/get
📖
Documentation Mutateurs
Rendez-vous sur MDN web docs pour en savoir plus sur les mutateurs.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Functions/set

14.4. Héritage

L’héritage est un mécanisme d’extension de classe. C’est une pratique peu employée en JavaScript, principalement en raison de sa nature modulaire et fonctionnelle.

L’héritage se caractérise par l’usage du mot-clé extends lors de la définition de la classe et aussi par l’utilisation de l’opérateur super() dans le constructeur.

class/extends.js
class Product {               // (1)
  constructor() {
    this.title = 'Sans titre';
  }
}

class Book extends Product {  // (2)
  constructor(options) {
    super(options);

    if (options.title) {
      this.title = options.title;
    }
  }
}

const book = new Book({ title: 'Node.js' });
console.log(book.title);    // (3)

const product = new Product({ title: 'Node.js' });
console.log(product.title); // (4)
  1. La classe Product affecte un titre par défaut lorsqu’un nouvel objet est initialisé.

  2. La classe Book affecte un titre donné en argument et, sinon, se base sur la valeur par défaut de la classe Product.

  3. Affiche 'Node.js'.

  4. Affiche 'Sans titre' – la propriété title ne se définit pas dans le constructeur (cf. class Product).

En pratique, c’est comme si on empilait les classes les unes sur les autres. On lègue des méthodes aux classes qui héritent. Si une méthode porte le même nom, la méthode “en haut de la pile” a la priorité.

L’appel à la fonction super() appelle le constructeur de la classe étendue. Si on ne l’appelle pas, le constructeur de la classe parente ne sera pas invoqué.

On reparlera de l’héritage dans le chapitre 9 avec un exemple populaire d’héritage appliqué aux composants visuels avec la bibliothèque React.

15. Coordonner des actions asynchrones (Promise)

Une promesse est un objet retourné immédiatement mais dont le résultat est obtenu plus tard, de manière asynchrone. Cette résolution est soit positive soit négative.

promise/base.js
const p = new Promise((resolve) => resolve('promesse tenue'));

console.log(p);                           // (1)
console.log('un');                        // (2)
p.then(message => console.log(message));  // (4)
console.log('deux');                      // (3)
  1. Affiche Promise – ce n’est pas le résultat que l’on voit, mais l’objet avec lequel interagir pour être prévenu de la mise à disposition du résultat.

  2. Affiche "un".

  3. Affiche "deux" – c’est parce que la ligne d’avant a mis en attente la fonction anonyme.

  4. Affiche "promesse tenue" en dernier.

💬
Design Pattern Executor

Le fait qu’une fonction nous passe d’autres fonctions pour commander un résultat s’appelle le pattern Executor.

Une Promise s’orchestre en deux temps :

  • L’initialisation
    On décide de la manière dont le traitement asynchrone sera effectué.

  • La résolution
    Positive en appelant resolve() ou négative, en appelant reject(). Le résultat passé à resolve() sera transmis au premier argument de then(). Le résultat passé à reject() sera transmis au deuxième argument de then(), mais aussi au premier argument de catch().

Une instance de Promise expose plusieurs méthodes pour propager le statut de son exécution :

then(onSuccess[, onError])

Fonction acceptant un callback de résolution et un autre de rejet (facultatif).

catch(onError)

Fonction acceptant un callback de rejet.

promise/then-catch.js
const oddTime = (date) => {
  return new Promise((resolve, reject) => {
    parseInt(date.getTime() / 1000) % 2                 // (1)
      ? resolve('le nombre de secondes est impair :-)')
      : reject('le nombre de secondes n\'est pas impair :-(');
  });
}

const now = new Date();

oddTime(now)                                            // (2)
  .then(msg => console.log(msg), msg => console.error(msg));

oddTime(new Date(now.getTime() + 1000))                 // (3)
  .then(msg => console.log(msg))                        // (4)
  .catch(msg => console.error(msg))                     // (5)
  1. La fonction oddTime() accepte un argument de type <<date,Date>. Elle résout la promesse positivement (resolve()) si le nombre de secondes est impair et négativement (reject) sinon.

  2. Utilisation de la forme compacte de then() avec deux callbacks : un de succès (associé à resolve()) et un d’échec (associé à reject()).

  3. On crée une nouvelle promesse, avec une date calée une seconde plus tard.

  4. Affiche "le nombre de secondes est impair :-)" puisque la résolution est positive.

  5. Affiche "le nombre de secondes n’est pas impair :-(" puisque la résolution est négative.

💬
Histoire Standard Promise/A+

Historiquement, de nombreuses bibliothèques ont proposé leur propre implémentation de promesses. Elles avaient le défaut de ne pas être interopérables. La spécification Promise/A+ (github.com/promises-aplus/promises-spec) a émergé pour établir un standard de compatibilité.

ECMAScript 2015 introduit nativement cette API. Il n’y a donc plus besoin de polyfill ou de bibliothèque pour en bénéficier.

En général, on utilise les promesses pour aller plus vite, parce qu’on peut continuer à traiter d’autres actions en attendant l’arrivée du résultat.

C’est comme quand on se rend au restaurant : les personnes en cuisine traitent des commandes (actions longues) tandis que les personnes au service gèrent des interactions plus courtes mais plus fréquentes. Au final, le ticket de commande contient la liste des promesses dont on attend la résolution.

Nous verrons d’autres utilisations des promesses dans le reste de l’ouvrage, plus particulièrement avec fetch() au chapitre 9 ainsi qu’avec promisify au chapitre 4.

📖
Documentation Promise
Rendez-vous sur MDN web docs pour en savoir plus sur les promesses.
developer.mozilla.org/docs/fr/Web/JavaScript/Reference/Global_Objects/Promise
💡
Lien Guide des promesses

Le guide www.w3.org/2001/tag/doc/promises-guide est très complet. Il est en anglais ainsi qu’en libre consultation sur le site du W3C.

Son dépôt GitHub github.com/w3ctag/promises-guide permet d’y contribuer.

15.1. Collection de promesses

Promise.all() est une méthode statique de la class Promise. Elle accepte un tableau de promesses et en retourne elle-même une promesse. Cette dernière est résolue positivement si toutes les promesses réussissent et négativement dès que l'une d’entre elles échoue.

promise/all.js
const asyncRandom = () => new Promise((resolve) => {  // (1)
  const timing = Math.floor(Math.random() * 2000);
  setTimeout(() => resolve(`résolu en ${timing}ms`), timing);
});

const all = Promise.all([                             // (2)
  asyncRandom(),
  asyncRandom(),
  asyncRandom()
]);

all.then(messages => console.log(messages));          // (3)
  1. Cette fonction résout la promesse après un délai aléatoire compris entre 0 et 2000 millisecondes.

  2. On passe trois promesses à Promise.all().

  3. La résolution est déclenchée dès que les trois promesses sont résolues – l’argument contient un tableau listant les résultats dans l’ordre initial des promesses.

L’exemple précédent illustre la parallélisation des actions. Si la promesse la plus longue est résolue en une seconde, alors le temps d’attente pour la résolution de toutes les promesses est de une seconde.
Si on avait été dans un enchaînement séquentiel, le temps d’attente final aurait été l’accumulation des temps d’attente de la résolution de chacune des promesses.

Les promesses sont un des meilleurs moyens à notre disposition pour modulariser, linéariser et clarifier le sens du flot de notre code.

15.2. async/await

Les opérateurs async et await aident à mettre en pause l’interpréteur ECMAScript, en attendant le résultat d’une fonction asynchrone (préfixée par async). Les promesses sont implicitement compatibles. On peut donc les mettre à plat pour obtenir un résultat sans avoir à utiliser then() ni `catch()`.

Transformons l’exemple de la section précédente pour comprendre l’impact de async et de `await`.

promise/async-await.js
function asyncRandom() {
  return new Promise((resolve) => {
    const timing = Math.floor(Math.random() * 2000);
    setTimeout(() => resolve(`résolu en ${timing}ms`), timing);
  });
}

(async () => {                  // (1)
  const all = [                 // (2)
    await asyncRandom(),
    await asyncRandom(),
    await asyncRandom()
  ];

  console.log(all);             // (3)
})();
  1. On crée une fonction asynchrone auto-invoquée – parce qu’on ne peut pas encore utiliser de fonction asynchrone directement au niveau principal d’un script.

  2. Chaque utilisation de await met l’interpréteur en pause.

  3. L’affichage du temps d’exécution de chaque promesse se fait lorsque les trois promesses sont résolues.

On gagne en lisibilité, mais on perd en vitesse. Les promesses sont exécutées séquentiellement et non en parallèle. Il est important d’arbitrer les choix de conception et d’éviter de bloquer l’exécution de vos scripts sans raison explicite.

16. Conclusion

ECMAScript est un langage bien plus riche, complet et élégant qu’il n’y paraît.

Ce chapitre nous a appris les différentes structures de langage communes à tous les environnements comprenant ECMAScript. Cela s’applique aussi bien à Node qu’aux navigateurs web.
Je vous invite à revenir à ce chapitre pour vous rafraîchir la mémoire, mais aussi pour jouer avec les exemples afin de confirmer votre compréhension du langage.

Dans le chapitre suivant, nous allons relier ces apprentissages avec Node – notre interpréteur et environnement d’exécution JavaScript.