💡
|
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.
-
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.
'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.
'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. |
'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.
'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.
Table de compatibilité | |
Spécification |
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) | |
Table de compatibilité (Node.js) | |
Spécification |
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.
'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.
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.
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.
null
On utilise null
pour signifier l'absence de valeur.
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.
[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.
{
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.
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.
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 :
const base_price = 13;
const base_price = 14;
💬
|
Question Une variable constante ?
Une variable variables/const-freeze.js
|
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.
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.
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.
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
.
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.
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.
// 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)
-
Utilisation de guillemets simples (
\n
sert à revenir à la ligne). -
Utilisation de guillemets doubles : évite d’échapper le guillemet simple.
-
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.
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
.
console.log('I ♥ JavaScript'.length); // (1)
console.log(''.length); // (2)
-
Affiche
14
. -
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 :
const mot = 'Node.js';
console.log(mot[0]); // (1)
console.log(mot.charAt(1)); // (2)
-
Affiche
N
. -
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 :
const mot = 'Node.js';
console.log(mot.toLocaleLowerCase()); // (1)
console.log(mot.toLocaleUpperCase()); // (2)
-
Affiche
node.js
. -
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 :
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)
-
Affiche
Node.js
. -
Affiche
Node.js
. -
Affiche
Node.js
. -
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 :
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)
-
Retourne
4
. -
Retourne
-1
– aucune occurrence n’a été trouvée. -
Retourne
5
– première occurrence de la lettrea
. -
Retourne
7
– dernière occurrence de la lettrea
.
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 :
/[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 |
Illustrons leur utilisation en identifiant du texte répondant (match
)
à une expression régulière (/…/
) :
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)
-
Identifie et affiche
Node
, l’occurrence contenue dans le motNode.js
. -
Affiche deux fois
ode
– les occurrences contenues dans les motsNode.js
etAnode
. -
Affiche
Node
etnode
en combinant les deux optionsi
etg
– les occurrences contenues dans les motsNode.js
etAnode
.
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*
.
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)
-
Affiche
["75"]
– les 2 premiers caractères numériques de la chaîne. -
Affiche
["75015"]
– les 5 premiers caractères numériques (satisfait la condition5
de{2,5}
). -
Affiche
["75015 Paris"]
. -
Affiche
["75015 Paris"]
– l’optioni
évite de préciser l’ensembleA-Z
. -
Affiche
["92410 Ville"]
– capture les caractères jusqu’à ce que la condition ne soit plus remplie en rencontrant le trait d’union (-
). -
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" …]
.
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)
-
Affiche
["75015 Paris"]
. -
Affiche
["75015 Paris", "75015"]
– le premier élément correspond à la chaîne identifiée tandis que le second correspond au premier groupe de capture. -
Affiche
["33900", "33900"]
– l’option multiligne itère de ligne en ligne jusqu’à trouver un motif. -
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.
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)
-
Affiche
["♥ RegExp", "RegExp"]
– et s’arrête là car l’espace suivant n’est pas un caractère de mot. -
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 :
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 :
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)
-
Affiche
"I love JavaScript"
– si le premier argument dereplace
est une chaîne, elle est convertie automatiquement en expression régulière. -
Affiche
"I ♥ PHP"
. -
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 :
const text = 'I ♥ JavaScript';
const shout = text.replace(/\b(\w+)$/u, (pattern, lang) => {
return lang.toLocaleUpperCase();
});
console.log(shout); // (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.
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)
-
Affiche
3
. -
Affiche
true
– la condition est vérifiée. -
Affiche
true
– c’est la valeur de la variablecheck
suite à son affectation à la ligne précédente. -
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 ( |
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))');
}
-
Affiche
false
– il s’agit d’une chaîne vide. -
Affiche
true
– il s’agit d’une valeur non nulle. -
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.
console.log(40);
console.log(40.0);
console.log(40 === 40.0); // (1)
console.log(40 === '40.0'); // (2)
-
Affiche
true
– les deux valeurs sont strictement équivalentes. -
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 :
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)
-
Affiche
Infinity
. -
Affiche
-Infinity
. -
Affiche
1.7976931348623157e+308
– le plus grand réel utilisable. -
Affiche
5e-324
– le plus petit réel utilisable. -
Affiche
9007199254740991
– le plus grand entier utilisable. -
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.
console.log(0x0000); // (1)
console.log(0x000A); // (2)
console.log(0x00A0); // (3)
console.log(0x0A00); // (4)
-
Affiche
0
. -
Affiche
10
– carA
en hexadécimal vaut 10 en décimal. -
Affiche
160
– pour10×16
(une “dizaine” vaut 16). -
Affiche
2560
– pour10×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.
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) |
|
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)
-
Affiche
6
. -
Affiche
-2
. -
Affiche
8
. -
Affiche
0.5
. -
Affiche
2
. -
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
|
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").
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.
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)
-
Affiche
true
. -
Affiche
true
. -
Affiche
false
. -
Affiche
false
.
🚨
|
Assertion
NaN n’est pas un nombre ?Il faut se méfier de number/nan-number.js
Il vaut mieux s’assurer qu’une variable est à la fois un nombre et
qu’elle ne vaut pas number/is-not-a-nan.js
|
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.
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)
-
Affiche
3
. -
Affiche
3.141592653589793
. -
Affiche
14
– ça ne change rien pourparseInt
. -
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.
console.log(parseInt(10, 16)); // (1)
console.log(parseInt('A', 16)); // (2)
console.log(parseInt('A00', 16)); // (3)
-
Affiche
16
. -
Affiche
10
–A
vaut10
en hexadécimal. -
Affiche
2560
– aurait pu s’écrire0xA00
.
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é. |
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)
-
Affiche
3
. -
Affiche
4
. -
Affiche
4
. -
Affiche
4
. -
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()
:
console.log(10.0101.toFixed(2)); // (1)
console.log(10.0101.toFixed(0)); // (2)
-
Affiche
'10.01'
. -
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 :
-
la création de la fonction ;
-
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.
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)
-
On crée la fonction
hello
. -
Affiche
[Function: hello]
– il s’agit de la définition de la fonction. -
Affiche
"Hello World"
– il s’agit de l'exécution de la fonction, qui retourne un résultat. -
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 valeurundefined
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.
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)
});
-
Affiche
"Deux secondes plus tard"
deux secondes après le début du script. -
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.
const printYear = (date) => { // (2)
console.log(date.getUTCFullYear()); // (3)
}
setTimeout(printYear, 1000, new Date()); // (1)
// équivalent à
// setTimeout(date => printYear(date), 1000, new Date());
-
Le troisième argument (et les suivants) de
setTimeout()
sont transmis en paramètres de la fonction de rappel (callback). -
Cette fonction est invoquée une seconde après le début du script, et reçoit en paramètre la date du moment.
-
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.
const combien_de = (nom, ...params) => {
console.log(`On a compté ${params.length} ${nom}.`);
};
combien_de('patates', 'un', 'deux', 'trois'); // (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.
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)
-
Affiche
["lundi", "mardi", "mercredi", "jeudi", "vendredi"]
. -
Affiche
5
– soit la longueur du tableau. -
Affiche
"mardi"
– un tableau commence à l’index0
. -
Affiche
undefined
– il n’y a aucun élément défini à l’index5
.
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.
console.log(Array.from('fromage')); // (1)
// utilisation du second argument
const uppercase = (letter) => letter.toUpperCase();
console.log(Array.from('fromage', uppercase)); // (2)
-
Affiche
["f", "r", "o", "m", "a", "g", "e"]
– chaque lettre de la chaîne. -
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()
:
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));
-
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 array/spread.js
|
À 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).
const headers = ['ID', 'NOM', 'PRENOM'];
console.log(headers.join()); // (1)
console.log(headers.join(';')); // (2)
console.log(headers.join('')); // (3)
-
Affiche
"ID,NOM,PRENOM"
– le séparateur par défaut est une virgule. -
Affiche
"ID;NOM;PRENOM"
– on a choisi le point-virgule comme séparateur. -
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.
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));
-
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
).
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)
}
-
Affiche successivement
0
puis1
. -
Affiche successivement
"samedi"
puis"dimanche"
– l’index sert à retrouver la valeur dans le tableau. -
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”.
const undeux = ['un', 'deux'];
const printIndex = (element, index, array) => {
console.log(`${element} : index ${index}`); // (2)
}
undeux.forEach(printIndex); // (1)
-
Applique la fonction
printIndex()
pour chaque élément du tableauundeux
. -
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é.
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)
-
Affiche
[1, 2, 3]
. -
Affiche
[ { label: "un", order: 1 }, { label: "deux", order: 2 } ]
– le tableau a été trié sur la valeur deorder
.
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.
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)
-
Affiche
["a", "A", "b", "c"]
– les majuscules influencent le tri. -
Affiche
[ { label: "deux", order: 2 }, { label: "un", order: 1 } ]
– le tableau a été trié sur la valeur delabel
.
💡
|
Alternative
Array.reverse()
La méthode array/reverse.js
|
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.
const newArray = ['a', ' b', 'c '].map(value => {
return value.trim().toUpperCase();
});
console.log(newArray); // (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 :
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)
-
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.
const stats = [2, 4, 6, 10];
const sum = (previous, element) => previous + element; // (2)
console.log(stats.reduce(sum, 0)); // (1)
-
Effectue une réduction à l’aide de la fonction
sum()
et d’une valeur par défaut de0
– affiche22
à l’issue des itérations . -
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.
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)
-
Retourne
[3]
– c’est la seule valeur qui soit un nombre. -
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
).
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)
-
Affiche
0
– le premier"un"
est l’élément ``0` du tableau. -
Affiche
1
– le premier"deux"
est l’élément ``1` du tableau. -
Affiche
-1
– cet élément est absent du tableau. -
Affiche
3
– le dernier"deux"
est l’élément ``3` du tableau. -
Affiche
true
– l’élément"un"
existe dans le tableau. -
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.
const values = [null, 2, 10, 100];
const biggerThan50 = (value) => value > 50; // (1)
console.log(values.find(biggerThan50)); // (2)
console.log(values.findIndex(biggerThan50)); // (3)
-
La fonction retourne
true
si la valeur passée en argument est un nombre supérieur à50
. -
Affiche
100
. -
Affiche
3
– c’est l’index de la valeur100
.
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.
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)
-
Affiche
false
– toutes les valeurs ne sont pas égales àundefined
. -
Affiche
true
– au moins une valeur est égale àundefined
. -
Affiche
false
– il n’y a plus de valeurundefined
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.
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)
-
Affiche
"lundi"
. -
Affiche
"mardi"
. -
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.
const weekdays = [
'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi'
];
const [first, second, ...rest] = weekdays;
console.log(rest); // (1)
// revient au même que
// const [,, ...rest] = weekdays;
-
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.
const values = ['un', 'deux', 'trois', 'quatre'];
console.log(values.slice(1)); // (1)
console.log(values.slice(1, 2)); // (2)
-
Affiche
["deux", "trois", "quatre"]
– à partir de l’index1
. -
Affiche
["deux"]
– à partir de l’index1
et jusqu’à l’index2
(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.
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)
-
Affiche
["quatre"]
– premier élément à partir de la fin. -
Affiche
["deux", "trois", "quatre"]
– les trois premiers éléments à partir de la fin. -
Affiche
["un", "deux", "trois"]
– jusqu’au dernier élément à partir de la fin (non inclus). -
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.
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)
-
Affiche
"Francine"
. -
On affecte une valeur numérique à l’index
age
une fois l’objet créé. -
Affiche
25
– la valeur numérique précédemment affectée. -
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.
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']);
-
Affecte la chaîne
@FrancineDu26
dans l’index correspondant à la valeur de la variableSOCIAL_NETWORK
. -
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.
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)
-
Affiche
"Drôme"
– on a décomposé la clélocation
. -
Affiche
"Francine"
– on a décomposé puis renommé la cléfirst_name
en une nouvelle variable :prenom
. -
Affiche
false
– on a décomposé la cléis_admin
et, comme elle n’existe pas, on a spécifié la valeur par défautfalse
, au lieu deundefined
.
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.
const francine = {
first_name: 'Francine',
location: 'Drôme',
twitter: '@FrancineDu26',
};
const {first_name, ...metadata} = francine;
console.log(first_name); // (1)
console.log(metadata); // (2)
-
Affiche
"Francine"
. -
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.
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)
-
Affiche
{ first_name: "Francine", location: "Drôme" }
– la nouvelle variable contient nos deux objets combinés. -
Affiche
{ first_name: "Francine" }
– ce sont les valeurs originelles de notre objet. -
Affiche
{ first_name: "Francine", location: 'Ardèche' }
– l’objeto2
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.
const francine = {
first_name: 'Francine',
};
const francine26 = {...francine, location: 'Drôme'};
console.log(francine26); // (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.
const francine = {
first_name: 'Francine',
location: 'Drôme',
};
console.log(Object.entries(francine)); // (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 :
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}`);
}
-
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.
const francine = {
first_name: 'Francine',
location: 'Drôme',
};
console.log(Object.keys(francine)); // (1)
console.log(Object.values(francine)); // (2)
-
Affiche
["first_name", "location"]
. -
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.
const francine = {
first_name: 'Francine',
location: 'Drôme',
};
console.log(francine.hasOwnProperty('location')); // (1)
console.log(francine.hasOwnProperty('twitter')); // (2)
-
Affiche
true
. -
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.
const francine = {
first_name: 'Francine',
location: 'Drôme',
};
console.log('location' in francine); // (1)
console.log('twitter' in francine); // (2)
-
Affiche
true
. -
Affiche
false
– cette clé n’existe pas dans cet objet.
Enfin, on peut tester la valeur associée avec la syntaxe standard objet.clé
.
const francine = {
first_name: 'Francine',
location: 'Drôme',
};
console.log(Boolean(francine.location)); // (1)
console.log(Boolean(francine.twitter)); // (2)
-
Affiche
true
. -
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.
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)
-
Affiche
false
– la valeurundefined
est convertie enfalse
. -
Affiche
false
– la clé existe bien, mais elle contient la valeurundefined
. -
Affiche
true
– le test se fait sur l’existence de la clé. -
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.
{
"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.
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)
-
Affiche
"Hello World!"
. -
Affiche
32
. -
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 :
const location = {
lat: 48.8503439,
lon: 2.34658949
}
console.log(JSON.stringify(location)); // (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 :
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)
-
Affiche
"\"geo=48.8503439,2.34658949\""
– c’est la sérialisation définie par notre fonctiontoJSON
. -
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.
const location = {
lat: 48.8503439,
lon: 2.34658949,
city: 'Paris',
toJSON() {
return `geo=${this.lat},${this.lon}`;
}
}
console.log(JSON.stringify(location)); // (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.
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)
-
On initialise l’objet date
past
au 04 décembre 2013. -
Affiche
2013
– l’année liée à l’objetpast
. -
Affiche
2022
– l’année liée à l’objetnow
(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.
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)
-
Change la date vers l’année
2015
. -
Affiche
"2015-12-04T10:00:00.000Z"
. -
Change la date vers le mois
1
. -
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.
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)
-
Affiche
04/12/2013
. -
Affiche
décembre
.
⚠️
|
Attention M01, M02, etc. ?
Si, en formatant une date, les caractères 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.
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)
-
Affiche
4 déc. 2013
. -
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 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, '');
}
}
-
Le constructeur reçoit un ou plusieurs argument(s) lors de l’instanciation de la classe.
-
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. -
toJSON()
est une méthode de la classe. -
isbn()
est un accesseur (préfixeget
) – une propriété dont la valeur est calculée à chaque fois qu’elle est appelée. -
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 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)
-
Affiche
false
– la propriété n’existe pas. -
Affiche
true
– la propriétéis_published
a été changée à la ligne précédente. -
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 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)
-
On appelle la méthode statique
Book.clean()
pour nettoyer le code EAN13. -
Affiche
"9782212139938"
– la valeur a bien été nettoyée. -
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.
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)
-
Définition de l’accesseur
isbn()
. -
Affiche
"9782212139938"
– c’est une propriété de l’objetnodebook
. -
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).
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)
-
Définition du mutateur
ean13()
– il accepte un seul argument. -
Affiche
978
– l’attribut a été créé lors de l’appel du mutateur. -
Affiche
2212139938
– idem. -
Affiche
undefined
– il faudrait créer un accesseurget 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 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)
-
La classe
Product
affecte un titre par défaut lorsqu’un nouvel objet est initialisé. -
La classe
Book
affecte un titre donné en argument et, sinon, se base sur la valeur par défaut de la classeProduct
. -
Affiche
'Node.js'
. -
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.
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)
-
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. -
Affiche
"un"
. -
Affiche
"deux"
– c’est parce que la ligne d’avant a mis en attente la fonction anonyme. -
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 appelantresolve()
ou négative, en appelantreject()
. Le résultat passé àresolve()
sera transmis au premier argument dethen()
. Le résultat passé àreject()
sera transmis au deuxième argument dethen()
, mais aussi au premier argument decatch()
.
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.
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)
-
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. -
Utilisation de la forme compacte de
then()
avec deux callbacks : un de succès (associé àresolve()
) et un d’échec (associé àreject()
). -
On crée une nouvelle promesse, avec une date calée une seconde plus tard.
-
Affiche
"le nombre de secondes est impair :-)"
puisque la résolution est positive. -
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.
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)
-
Cette fonction résout la promesse après un délai aléatoire compris entre 0 et 2000 millisecondes.
-
On passe trois promesses à
Promise.all()
. -
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`.
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)
})();
-
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.
-
Chaque utilisation de
await
met l’interpréteur en pause. -
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.