JavaScript est un langage de programmation. Là où le HTML décrit le contenu et le CSS décrit son apparence, JavaScript agit : il calcule, il décide, il réagit à ce que fait la personne qui visite la page, et il peut modifier la page à tout moment sans la recharger.
Reprenons la maison des deux cours précédents : le HTML en est le gros œuvre, le CSS la décoration. JavaScript, c'est l'électricité et la domotique : l'interrupteur qui allume la lumière, le volet qui se ferme tout seul à la tombée de la nuit, le thermostat qui ajuste le chauffage. La maison existe sans lui, mais elle ne réagit à rien.
« Voici un bouton Demander un devis. »
« Ce bouton est cuivre, arrondi, et fonce au survol. »
« Au clic, je calcule le devis et je l'affiche sous le bouton. »
Sur un site comme celui de l'atelier, JavaScript peut par exemple :
Et il ne vit plus seulement dans le navigateur : grâce à Node.js, le même langage fait aussi tourner des serveurs, des outils en ligne de commande ou des applications de bureau (VS Code, que tu utilises pour coder, est lui-même écrit en grande partie en JavaScript !). Dans ce cours, on reste dans le navigateur.
Il existe deux grandes familles de langages. Les langages compilés (C, Rust…) sont traduits en entier, une fois pour toutes, en un programme exécutable, comme un livre qu'on ferait traduire avant de l'imprimer. Les langages interprétés, comme JavaScript, sont lus et exécutés directement par un programme, ici le moteur JavaScript du navigateur, comme un interprète qui traduit un discours au fil de la parole.
On dit aussi que JavaScript est un langage de haut niveau : tu manipules des notions humaines (du texte, des nombres, des listes) sans jamais t'occuper de la mémoire de l'ordinateur. C'est le moteur qui réserve la place nécessaire et qui la libère quand plus rien ne s'en sert, grâce à un mécanisme appelé ramasse-miettes (on en reparlera dans la partie 3).
| Année | Événement |
|---|---|
| 1995 | Brendan Eich crée le langage chez Netscape, en une dizaine de jours. Il s'appelle d'abord Mocha, puis LiveScript, puis JavaScript. |
| 1997 | Le langage est normalisé sous le nom d'ECMAScript (norme ECMA-262) pour que tous les navigateurs parlent la même langue. |
| 2015 | La version ES2015 (souvent appelée « ES6 ») modernise tout : let, const, fonctions fléchées, classes, modules… |
| Depuis | Une nouvelle version sort chaque année, avec quelques nouveautés à chaque fois. Ce cours utilise le JavaScript moderne. |
Voici ton tout premier programme. Il tient en une ligne : il demande d'afficher un message dans la console. Le cadre noir sous le code est la console : le code a déjà été exécuté au chargement de la page, et tu vois son résultat.
console.log("Bienvenue à l'atelier !");
Chaque exemple dispose de trois boutons :
Comme pour le CSS, il existe trois façons de relier du JavaScript à une page HTML : directement dans une balise, dans la page, ou dans un fichier à part. Et comme pour le CSS, l'une d'elles est nettement meilleure que les autres.
Certains attributs HTML, comme onclick, acceptent du
JavaScript comme valeur. Le code s'exécute quand l'événement se produit :
<button type="button" onclick="alert('Votre PC est prêt à être récupéré !')">
Où en est ma réparation ?
</button>
👉 Clique sur le bouton : une petite fenêtre s'ouvre.
style en CSS.
Pratique pour un test, mais le comportement se retrouve éparpillé dans
le HTML, impossible à réutiliser, et beaucoup de sites sécurisés bloquent
carrément ce type de code. On verra au chapitre sur les événements la
bonne manière de réagir à un clic.
La balise <script> peut contenir directement du code.
On la place en général tout à la fin du
<body>, juste avant sa balise fermante :
<body>
<h1>L'Atelier Informatique du Finistère</h1>
<p>Réparation et montage de PC à Morlaix.</p>
<script>
console.log("La page est chargée, le script démarre.");
</script>
</body>
Pourquoi à la fin ? Parce que le navigateur lit la page de haut en
bas. Quand il rencontre un <script>, il s'arrête
pour l'exécuter avant de continuer. Placé tout en haut, le script
retarderait l'affichage de la page, et surtout il ne pourrait pas agir sur
des éléments qui n'existent pas encore.
<script> interne, tout en bas. Fais
Ctrl+U et descends jusqu'à la fin pour le voir.
Le code est rangé dans un fichier à part, avec l'extension
.js, et la page le charge grâce à l'attribut
src. La balise reste vide :
<script src="scripts/devis.js"></script>
Et dans le fichier scripts/devis.js, du JavaScript pur, sans aucune balise :
// scripts/devis.js
console.log("Le module de devis est prêt.");
Exactement comme pour une feuille de style externe, on y gagne :
<script> n'est jamais une balise
auto-fermante. <script src="devis.js" /> ne fonctionne
pas : la balise fermante </script> est obligatoire,
même quand il n'y a rien entre les deux.
Mettre le script à la fin du <body> marche très bien.
Mais la méthode moderne consiste à le déclarer dans le
<head> avec l'attribut defer
(« différer ») :
<head>
<meta charset="UTF-8">
<title>Demander un devis</title>
<link rel="stylesheet" href="styles/style.css">
<script src="scripts/devis.js" defer></script>
</head>
Le navigateur télécharge alors le fichier en parallèle de la lecture de la page, et ne l'exécute qu'une fois tout le HTML lu. On a le meilleur des deux mondes : le téléchargement commence au plus tôt, et le script trouve la page complète.
| Écriture | Téléchargement | Exécution | Usage |
|---|---|---|---|
<script src> seul | bloque la lecture de la page | immédiate | à la fin du <body> uniquement |
defer | en parallèle | une fois la page lue, dans l'ordre des scripts | le choix par défaut |
async | en parallèle | dès que le fichier arrive, dans n'importe quel ordre | scripts indépendants (statistiques de visite…) |
<script type="module">. Ce type de
script se comporte automatiquement comme defer et permet de
découper son code en plusieurs fichiers qui s'importent les uns les
autres. On le verra dans la partie 5.
index.html
(le squelette du cours HTML suffit) et un fichier
scripts/main.js contenant une ligne
console.log("Mon premier script externe !");.
Relie-les avec defer, ouvre la page dans ton navigateur,
puis appuie sur F12 et va dans l'onglet Console :
ton message doit y être.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Mon premier script</title>
<script src="scripts/main.js" defer></script>
</head>
<body>
<h1>Ouvre la console avec F12</h1>
</body>
</html>
Si rien n'apparaît, vérifie le chemin dans src : un
fichier introuvable provoque une erreur 404 visible dans l'onglet
Réseau (ou en rouge dans la console).
Quand on apprend à programmer, on a besoin de voir ce que fait le code : la valeur d'un calcul, le contenu d'une variable, le moment où une ligne s'exécute. L'outil n°1 pour ça est la console du navigateur.
La console a deux usages. Elle affiche les messages
envoyés par les scripts de la page (et les erreurs, en rouge). Et elle
permet de taper du JavaScript directement, ligne par
ligne, pour tester une idée. Essaie : tape 2 + 3 puis
Entrée, elle répond 5. C'est la calculatrice la
plus puissante que tu aies jamais eue.
allow pasting. C'est une protection : des
escrocs demandent parfois à leurs victimes de coller du code dans la
console pour leur voler leur compte. Règle d'or : ne colle
jamais dans ta console un code qu'un inconnu t'envoie.
console.log() affiche ce qu'on met entre ses parenthèses. On
peut lui donner plusieurs valeurs séparées par des virgules : elles
s'affichent à la suite, séparées par un espace.
console.log("Diagnostic : gratuit");
console.log("Prix du devis :", 20, "€");
console.log(12 * 3);
console.log("12 * 3");
Regarde bien les deux dernières lignes. Sans guillemets,
12 * 3 est un calcul : JavaScript l'effectue
et affiche 36. Avec des guillemets, c'est un simple
texte, affiché tel quel. La console colore d'ailleurs les
nombres différemment des textes : un indice précieux quand on cherche
pourquoi un calcul se comporte bizarrement (on verra ça au chapitre 05).
log n'est pas seul. L'objet console propose
d'autres méthodes pour classer ses messages par importance :
console.info("Information : le stock de SSD a été mis à jour.");
console.warn("Attention : plus que 2 barrettes de RAM en stock.");
console.error("Erreur : impossible de joindre le fournisseur.");
Et console.table() présente une liste sous forme de tableau,
très pratique pour relire des données d'un coup d'œil :
console.table(["Diagnostic", "Devis détaillé", "Intervention"]);
| Méthode | Effet |
|---|---|
console.log() | message ordinaire (90 % de l'usage) |
console.info() | message d'information |
console.warn() | avertissement, sur fond jaune |
console.error() | erreur, sur fond rouge |
console.table() | affiche une liste ou un objet sous forme de tableau |
console.clear() | vide la console |
La console est réservée au développeur : le visiteur, lui, ne l'ouvre jamais. Pour lui adresser un message, le navigateur propose trois petites fenêtres toutes prêtes. Ces exemples ne se lancent pas tout seuls : clique sur Exécuter.
alert("Votre ordinateur est prêt. Vous pouvez passer le récupérer à l'atelier.");
console.log("Le message a été fermé.");
const reponse = confirm("Souhaitez-vous un devis détaillé (20 € déductibles) ?");
console.log("Réponse du client :", reponse);
const prenom = prompt("Quel est votre prénom ?");
console.log("Prénom saisi :", prenom);
confirm() renvoie true (OK) ou
false (Annuler) ; prompt() renvoie le texte
tapé, ou null si on annule. Le mot const sert
à ranger la réponse pour pouvoir la réutiliser : c'est le sujet du
chapitre 04.
Un commentaire est un texte que le moteur JavaScript ignore. Il sert à expliquer le code aux humains, y compris à toi dans six mois. Il en existe deux sortes :
// Commentaire sur une ligne : tout ce qui suit // est ignoré.
console.log("Ouverture de l'atelier : 9 h"); // possible aussi en fin de ligne
/*
Commentaire sur plusieurs lignes :
utile pour un paragraphe d'explication
ou pour désactiver un bloc de code entier.
*/
// console.log("Cette ligne est désactivée, elle ne s'affiche pas.");
Un bon commentaire explique pourquoi, pas quoi. Le code dit déjà ce qu'il fait ; le commentaire donne la raison qui n'est pas visible :
// ✗ Inutile : répète le code
// affiche 20
console.log(20);
// ✓ Utile : donne une information que le code ne dit pas
// Les 20 € du devis sont déduits de la facture si le client valide l'intervention.
console.log(20);
Un programme est une suite d'instructions, exécutées dans
l'ordre, de haut en bas. Chaque instruction se termine par un
point-virgule ;. JavaScript sait en ajouter
automatiquement quand on les oublie, mais cette correction automatique
réserve parfois des surprises : prends l'habitude de toujours les écrire.
Surtout, JavaScript est sensible à la casse : majuscules
et minuscules ne sont pas interchangeables. Regarde ce qui arrive avec un
simple L majuscule :
console.log("Cette ligne s'affiche.");
console.Log("Celle-ci provoque une erreur.");
console.log("Et celle-ci n'est jamais atteinte.");
Voici ta première erreur, et elle est riche
d'enseignements. Le message console.Log is not a function
(« console.Log n'est pas une fonction ») désigne précisément le coupable.
Et une erreur arrête le script : la troisième ligne ne
s'exécute jamais. Retiens ce réflexe : quand « plus rien ne marche »,
ouvre la console et lis le premier message rouge. Il indique presque
toujours le fichier et le numéro de ligne fautifs.
console.log("Cette ligne s'affiche.");
console.log("Celle-ci aussi, maintenant.");
console.log("Et celle-ci est enfin atteinte.");
// Inventaire annuel du stock de pièces détachées.
console.warn("Fermeture exceptionnelle samedi");
Un programme passe son temps à manipuler des informations : un prix, un nom de client, un nombre de pièces en stock. Pour les retrouver et les réutiliser, on les range dans des variables.
Imagine les bacs de rangement de l'atelier. Chaque bac porte une étiquette (« SSD 1 To », « Vis M2 ») et contient quelque chose. Une variable, c'est exactement ça : un nom qui désigne une valeur. On écrit l'étiquette une fois, puis on n'utilise plus que le nom.
let client; // 1. déclaration : on crée le bac « client », vide
client = "Mme Le Gall"; // 2. affectation : on met une valeur dedans
console.log(client);
let appareil = "PC portable"; // les deux en une fois : on « initialise »
console.log(appareil);
Trois mots de vocabulaire à retenir :
let) et un nom ;= ;= ne veut pas dire « est égal à »
mais « reçoit ». client = "Mme Le Gall" se lit
« client reçoit Mme Le Gall ». Pour comparer deux valeurs, on
utilisera === (chapitre 08).
Une variable déclarée avec let peut recevoir une nouvelle
valeur autant de fois qu'on veut. L'ancienne est simplement remplacée.
Attention, on n'écrit let qu'une seule fois,
à la création :
let ssdEnStock = 8;
console.log("Début de semaine :", ssdEnStock);
ssdEnStock = 5; // trois SSD posés lundi
console.log("Mardi matin :", ssdEnStock);
ssdEnStock = 12; // livraison du fournisseur
console.log("Mercredi :", ssdEnStock);
Une variable peut aussi recevoir le résultat d'un calcul, y compris un calcul qui utilise sa propre valeur :
let barrettesRam = 10;
barrettesRam = barrettesRam - 2; // on prend l'ancienne valeur, on retire 2
console.log(barrettesRam);
const (pour constante) déclare une variable qui ne
pourra plus jamais recevoir une autre valeur. Si on
essaie, JavaScript refuse et lève une erreur :
const TARIF_DEVIS = 20;
console.log("Le devis coûte", TARIF_DEVIS, "€");
TARIF_DEVIS = 25; // interdit !
console.log("Cette ligne ne s'affiche jamais.");
Conséquence logique : une constante doit recevoir sa valeur
dès sa déclaration. const ville; tout seul
est une erreur de syntaxe.
const par défaut, et ne passe à
let que lorsque tu sais que la valeur devra changer (un
compteur, un total qui s'accumule, un stock). En lisant ton code, on sait
alors immédiatement quelles valeurs bougent et lesquelles sont fixes. Moins
de surprises, moins de bugs.
Avant 2015, var était le seul mot-clé disponible. Tu le
croiseras dans les vieux tutoriels et les anciens sites, d'où l'intérêt de
le connaître. Mais il a des défauts qui ont justement poussé à créer
let et const.
Défaut n°1 : on peut le redéclarer sans le voir. Dans un long fichier, on écrase une variable existante sans aucun avertissement :
var technicien = "Shooby";
// … 200 lignes plus loin, quelqu'un réutilise le même nom …
var technicien = "stagiaire";
console.log(technicien); // la première valeur est perdue, sans prévenir
Avec let, l'erreur est signalée tout de suite, avant même que le script démarre :
let technicien = "Shooby";
let technicien = "stagiaire";
console.log(technicien);
Défaut n°2 : on peut l'utiliser avant de l'avoir déclaré.
JavaScript « remonte » la déclaration des var en haut du
script (on parle de hoisting, ou « hissage »), mais pas leur
valeur. Résultat : on obtient undefined au lieu d'une erreur,
et le bug passe inaperçu.
console.log(marqueSsd); // pas d'erreur… mais pas de valeur non plus
var marqueSsd = "Samsung";
console.log(marqueSsd);
Avec let (ou const), l'accès prématuré est clairement interdit :
console.log(marqueSsd);
let marqueSsd = "Samsung";
Un bloc est une portion de code entre accolades
{ }. Tu en verras partout à partir de la partie 2 (conditions,
boucles, fonctions). Une variable déclarée avec let ou
const n'existe qu'à l'intérieur de son bloc :
c'est sa portée. var, lui, s'en échappe :
défaut n°3.
{
var fuite = "je sors du bloc";
let sage = "je reste dans mon bloc";
console.log("Dans le bloc :", fuite, "/", sage);
}
console.log("Hors du bloc :", fuite);
console.log("Hors du bloc :", sage); // n'existe plus ici
Ce cloisonnement est une excellente chose : une variable temporaire utilisée dans un bloc ne risque pas d'écraser une variable du même nom ailleurs dans le programme. Chaque bac reste dans son armoire.
const | let | var | |
|---|---|---|---|
| Changer la valeur | ✗ non | ✓ oui | ✓ oui |
| Redéclarer le même nom | ✗ erreur | ✗ erreur | ⚠ oui, sans prévenir |
| Utiliser avant la déclaration | ✗ erreur | ✗ erreur | ⚠ oui, vaut undefined |
| Portée | le bloc | le bloc | ⚠ la fonction entière |
| Verdict | par défaut | si la valeur change | à éviter |
Le nom d'une variable doit respecter quelques règles strictes :
_ ou $, mais pas d'espace ni de tiret ;let, if, class, return…) ;prix, Prix et PRIX sont trois variables différentes.let class = "Première";
Et au-delà des règles, il y a les conventions, que tout le monde suit :
| Style | Exemple | Pour quoi ? |
|---|---|---|
| camelCase | prixDevis, nombreDePieces | presque tout : variables, fonctions |
| MAJUSCULES_SOULIGNÉES | TARIF_DEVIS, CODE_POSTAL_ATELIER | constantes de configuration, connues avant l'exécution |
| PascalCase | Client, FicheReparation | les classes (partie 3) |
| À éviter | Pourquoi | Mieux |
|---|---|---|
p, x, truc | ne dit rien du contenu | prixHorsRemise |
2emeClient | commence par un chiffre : erreur | deuxiemeClient |
nom-client | le tiret est lu comme une soustraction | nomClient |
donnees2 | un numéro n'explique pas la différence | donneesFournisseur |
téléphone | autorisé, mais les accents posent problème sur certains claviers et outils | telephone |
dateDeRestitution ne te coûte rien, et tu te
remercieras en relisant ton code.
statut qui vaut d'abord « reçu », puis
« en diagnostic », puis « prêt ». Affiche le statut après
chaque changement. Choisis bien entre let et
const !
const nomClient = "M. Kerboriou";
const TARIF_DEVIS = 20;
let statut = "reçu";
console.log(nomClient, "—", statut);
statut = "en diagnostic";
console.log(nomClient, "—", statut);
statut = "prêt";
console.log(nomClient, "—", statut, "— devis :", TARIF_DEVIS, "€");
Le nom du client et le tarif ne changent pas : const.
Le statut évolue : let.
Toutes les valeurs n'ont pas la même nature. On peut multiplier
20 par 3, mais pas "Morlaix". On peut
mettre "Morlaix" en majuscules, mais pas 20. Cette
nature s'appelle le type de la valeur, et c'est lui qui
décide de ce qu'on peut en faire.
JavaScript possède sept types primitifs, les briques de base, et un huitième type, l'objet, qui sert à les assembler (tableaux, fiches client… : c'est la partie 3).
| Type | Contient | Exemple |
|---|---|---|
string | du texte, entre guillemets | "Carte graphique" |
number | un nombre, entier ou à virgule | 29, 89.90 |
bigint | un entier immense, suivi d'un n | 90071992547409930n |
boolean | vrai ou faux, et rien d'autre | true, false |
undefined | « pas encore de valeur » | undefined |
null | « vide, volontairement » | null |
symbol | un identifiant unique | Symbol("id") |
L'opérateur typeof renvoie le nom du type, sous forme de texte :
console.log(typeof "Morlaix");
console.log(typeof 29);
console.log(typeof 29n);
console.log(typeof true);
console.log(typeof undefined);
console.log(typeof Symbol("id"));
console.log(typeof null); // surprise…
typeof null répond "object". C'est une erreur
commise dans la toute première version du langage, en 1995. On ne peut
plus la corriger sans casser des millions de sites existants : elle est
donc restée. Pour savoir si une valeur est null, on écrit
simplement valeur === null.
Dans certains langages, on doit annoncer le type d'une variable à sa création, et il ne peut plus changer. En JavaScript, non : le type est porté par la valeur, pas par la variable. Une même variable peut contenir un nombre, puis un texte. On dit que le typage est dynamique.
let reference = 4070;
console.log(reference, "→", typeof reference);
reference = "RTX 4070";
console.log(reference, "→", typeof reference);
Un booléen ne peut valoir que true (vrai) ou
false (faux). C'est le type des réponses par oui ou non, et le
résultat de toute comparaison :
const garantieValide = true;
const ssdEnStock = 3;
console.log(garantieValide);
console.log(ssdEnStock > 0); // reste-t-il des SSD ?
console.log(ssdEnStock > 10); // en reste-t-il plus de 10 ?
Plus tard, les conditions (if) accepteront n'importe quelle
valeur, et la convertiront en booléen pour décider. Il faut donc savoir
comment se fait cette conversion. La règle est courte : seules
huit valeurs deviennent false. On les appelle
les valeurs falsy (« fausses »). Tout le reste est
truthy (« vrai »).
// Les 8 valeurs falsy, converties avec Boolean()
console.log(Boolean(false), Boolean(0), Boolean(-0), Boolean(0n));
console.log(Boolean(""), Boolean(null), Boolean(undefined), Boolean(NaN));
// Tout le reste est truthy… même ce qui « ressemble » à du faux !
console.log(Boolean("0"), Boolean("false"), Boolean(" "), Boolean(-1));
"0" et le texte "false" sont
truthy : ce sont des chaînes non vides, peu importe ce
qu'elles racontent. Seule la chaîne vide "" est falsy. Ce
piège revient souvent avec les formulaires, où tout ce qui est saisi
arrive sous forme de texte.
Les deux signifient « pas de valeur », mais pas pour la même raison.
Reprenons nos bacs : undefined, c'est un bac
qu'on n'a jamais rempli ; null, c'est un bac
sur lequel on a volontairement écrit « vide ».
let dateDeRestitution; // déclarée, jamais remplie
console.log(dateDeRestitution); // JavaScript met undefined tout seul
let technicienAssigne = null; // « personne pour l'instant », dit exprès
console.log(technicienAssigne);
En pratique : undefined est la valeur que
JavaScript donne par défaut ; null est la
valeur que toi tu choisis pour dire « rien, et c'est
normal ». N'affecte jamais undefined à la main : utilise
null.
Un symbol est une valeur garantie unique : même avec la même description, deux symboles ne sont jamais égaux. Ils servent à créer des identifiants qui ne risquent aucune collision, un usage avancé qu'on retrouvera avec les objets.
const badgeA = Symbol("badge");
const badgeB = Symbol("badge");
console.log(badgeA);
console.log(badgeA === badgeB); // jamais égaux
console.log(badgeA.description); // la description sert juste à s'y retrouver
Le type bigint stocke des entiers trop grands pour le type
number. On le reconnaît au n final :
123n. Il est présenté en détail au
chapitre 07.
Deux propriétés distinguent les primitifs des objets. D'abord, quand on copie un primitif dans une autre variable, on copie la valeur : les deux variables deviennent indépendantes.
let piecePrevue = "SSD 500 Go";
let pieceMontee = piecePrevue; // copie de la valeur
pieceMontee = "SSD 1 To"; // le client a choisi plus grand
console.log("Prévue :", piecePrevue); // inchangée
console.log("Montée :", pieceMontee);
Ensuite, un primitif est immuable : on ne modifie jamais une valeur, on la remplace par une nouvelle. Ça paraît abstrait, mais tu le constateras concrètement avec les chaînes au chapitre suivant. Les objets, eux, se comportent autrement : on y reviendra dans la partie 3, car c'est une source de bugs classique.
console.log(typeof "29");
console.log(typeof 29.5);
console.log(typeof false);
console.log(Boolean(""));
console.log(Boolean("vide"));
console.log(Boolean(0));
let garantie;
console.log(garantie);
"29" est entre guillemets : c'est un
string, pas un nombre. 29.5 est un
number (il n'y a pas de type « décimal » à part).
"" et 0 sont falsy, "vide" est
une chaîne non vide donc truthy. Enfin, garantie est
déclarée sans valeur : undefined.
Une chaîne de caractères (string en anglais) est un texte : un nom, une adresse, un message, une référence de pièce. C'est sans doute le type que tu manipuleras le plus.
const ville = "Morlaix"; // guillemets doubles
const departement = 'Finistère'; // guillemets simples (apostrophes)
const region = `Bretagne`; // accents graves : les « gabarits » (voir 6.4)
console.log(ville, departement, region);
Guillemets doubles ou simples, c'est strictement la même chose. Choisis un style et tiens-t'y. En français, les guillemets doubles sont souvent plus pratiques : nos textes sont pleins d'apostrophes, et une apostrophe à l'intérieur de guillemets simples fermerait la chaîne.
const ok = "L'atelier ouvre à 9 h"; // ✓ aucun souci
const casse = 'L'atelier ouvre à 9 h'; // ✗ la chaîne s'arrête après « L »
Pour glisser dans une chaîne un caractère qui poserait problème, on le fait
précéder d'une barre oblique inversée \
(AltGr+8 sur un clavier français). On dit qu'on
l'échappe.
console.log('L\'atelier ouvre à 9 h');
console.log("Il a dit : \"Mon PC ne démarre plus\".");
console.log("Horaires :\n Lundi\t9 h – 18 h\n Samedi\t9 h – 12 h");
console.log("Dossier : C:\\Clients\\Kerboriou");
| Séquence | Produit |
|---|---|
\' et \" | une apostrophe ou un guillemet, sans fermer la chaîne |
\n | un retour à la ligne (new line) |
\t | une tabulation |
\\ | une barre oblique inversée |
\u{1F527} | n'importe quel caractère Unicode par son code (ici 🔧) |
Le signe + entre deux chaînes les colle bout à bout. On parle
de concaténation. Et += ajoute un morceau à la
fin d'une chaîne existante.
const prenom = "Yann";
const nom = "Le Bihan";
console.log("Client : " + prenom + " " + nom);
let message = "Bonjour";
message += ", votre PC est prêt";
message += ".";
console.log(message);
prenom + nom donne
"YannLe Bihan". Et dès qu'un des deux côtés est une chaîne,
+ concatène au lieu d'additionner :
"Total : " + 20 + 10 donne "Total : 2010" !
Le chapitre 07 explique pourquoi.
Les chaînes écrites entre accents graves
` (AltGr+7) s'appellent des
gabarits (template literals). On y insère
directement une variable, ou même un calcul, avec ${…}. Fini
les + et les espaces oubliés :
const client = "Yann Le Bihan";
const appareil = "PC portable";
const heures = 2;
const TAUX_HORAIRE = 45;
console.log(`Bonjour ${client}, votre ${appareil} est prêt.`);
console.log(`Main d'œuvre : ${heures} h × ${TAUX_HORAIRE} € = ${heures * TAUX_HORAIRE} €`);
Bonus : un gabarit peut s'étaler sur plusieurs lignes, sans aucun \n :
const ticket = 42;
const recu = `Atelier Informatique du Finistère
Ticket n° ${ticket}
Diagnostic : gratuit`;
console.log(recu);
Une chaîne est une suite de caractères numérotés à partir de
zéro. La propriété length donne leur nombre,
et les crochets [ ] permettent d'en lire un :
const reference = "SSD-NVME-1TO";
console.log(reference.length); // nombre de caractères
console.log(reference[0]); // le premier : position 0
console.log(reference[4]); // le cinquième : position 4
console.log(reference.at(-1)); // le dernier (at accepte les positions négatives)
console.log(reference[50]); // au-delà de la fin : undefined
0, donc le dernier est
à la position length - 1, et non length. Cette
numérotation à partir de zéro vaut partout en JavaScript, notamment pour
les tableaux.
Et comme annoncé au chapitre précédent, les chaînes sont immuables : on ne peut pas changer un caractère sur place. On fabrique une nouvelle chaîne.
let modele = "ryzen 7";
modele[0] = "R"; // ignoré, sans erreur
console.log(modele);
modele = "R" + modele.slice(1); // on crée une nouvelle chaîne
console.log(modele);
Une méthode est une action qu'on demande à une valeur,
avec la syntaxe valeur.methode(). Les chaînes en ont plusieurs
dizaines ; voici celles que tu utiliseras vraiment. Aucune ne modifie la
chaîne d'origine : elles renvoient une nouvelle chaîne.
Changer la casse, par exemple pour comparer une saisie sans se soucier des majuscules :
const saisie = "Morlaix";
console.log(saisie.toUpperCase());
console.log(saisie.toLowerCase());
console.log(saisie.toLowerCase() === "MORLAIX".toLowerCase());
Nettoyer les espaces superflus qu'on tape sans le vouloir dans un formulaire :
const email = " contact@ai29.tech ";
console.log(`[${email}]`);
console.log(`[${email.trim()}]`); // les deux côtés
console.log(`[${email.trimStart()}]`); // seulement le début
console.log(`[${email.trimEnd()}]`); // seulement la fin
Chercher un morceau de texte :
const panne = "L'écran reste noir au démarrage";
console.log(panne.includes("écran")); // contient ?
console.log(panne.startsWith("L'écran")); // commence par ?
console.log(panne.endsWith("démarrage")); // finit par ?
console.log(panne.indexOf("noir")); // à quelle position ?
console.log(panne.indexOf("clavier")); // -1 : introuvable
Extraire une portion avec slice(début, fin). La position de fin est exclue, et les positions négatives comptent depuis la fin :
const fichier = "facture-2026-0042.pdf";
console.log(fichier.slice(0, 7)); // de 0 à 7 (exclu)
console.log(fichier.slice(8, 12)); // l'année
console.log(fichier.slice(-4)); // les 4 derniers caractères
console.log(fichier.slice(8)); // de 8 jusqu'à la fin
Transformer : remplacer, découper, répéter, compléter :
const horaires = "9h-12h / 14h-18h";
console.log(horaires.replace("h", " h")); // seulement la 1re occurrence
console.log(horaires.replaceAll("h", " h")); // toutes
console.log("SSD,RAM,Alimentation".split(",")); // découpe en liste (partie 3)
console.log("=".repeat(20));
const numero = "42";
console.log(numero.padStart(4, "0")); // complète à gauche jusqu'à 4 caractères
console.log("Total".padEnd(10, ".") + " 65 €");
| Méthode | Renvoie |
|---|---|
length (propriété, sans parenthèses) | le nombre de caractères |
at(i) | le caractère à la position i (négatif = depuis la fin) |
toUpperCase() / toLowerCase() | la chaîne en majuscules / minuscules |
trim(), trimStart(), trimEnd() | la chaîne sans les espaces au bord |
includes(t), startsWith(t), endsWith(t) | true ou false |
indexOf(t) | la position de t, ou -1 |
slice(début, fin) | la portion entre deux positions |
replace(a, b) / replaceAll(a, b) | la chaîne avec a remplacé par b |
split(séparateur) | une liste de morceaux |
repeat(n) | la chaîne répétée n fois |
padStart(n, c) / padEnd(n, c) | la chaîne complétée jusqu'à n caractères |
saisie.trim().toLowerCase() nettoie puis met en
minuscules. On lit de gauche à droite, comme une chaîne de montage.
const numero = 7; et de
const saisieClient = " kerboriou ";, affiche la ligne
Ticket R-0007 — KERBORIOU. Indices : un gabarit,
String() pour transformer le nombre en texte,
padStart(), trim() et
toUpperCase().
const numero = 7;
const saisieClient = " kerboriou ";
const numeroTicket = String(numero).padStart(4, "0");
const nomClient = saisieClient.trim().toUpperCase();
console.log(`Ticket R-${numeroTicket} — ${nomClient}`);
Contrairement à d'autres langages, JavaScript n'a qu'un seul type pour les
nombres courants : number. Entiers, nombres à virgule,
négatifs, tout y passe.
const codePostal = 29600; // entier
const prixSsd = 89.90; // à virgule… écrite avec un POINT
const temperature = -4; // négatif
const octetsParTo = 1_000_000_000_000; // le _ sépare les milliers, pour la lisibilité
const microseconde = 1e-6; // notation scientifique : 1 × 10⁻⁶
const kilo = 1.2e3; // 1,2 × 10³
console.log(codePostal, prixSsd, temperature);
console.log(octetsParTo, microseconde, kilo);
89,90 n'est pas un nombre : la virgule y a un
autre sens en JavaScript. Pareil à l'affichage : console.log
montre 89.9. Pour afficher un prix à la française, voir
7.6.
Lance ce code. Le résultat va t'étonner :
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);
Ce n'est pas un bug de JavaScript : c'est la façon dont tous les ordinateurs stockent les nombres à virgule, en binaire. Certains nombres simples pour nous, comme 0,1, ne tombent pas juste en binaire (comme 1/3 ne tombe pas juste en décimal : 0,3333…). Il reste une infime erreur d'arrondi.
// En euros : imprécis
console.log(1.10 + 2.20);
// En centimes : exact
const totalCentimes = 110 + 220;
console.log(totalCentimes / 100);
Math est une boîte à outils toujours disponible, qui regroupe
les opérations mathématiques courantes :
const prix = 89.56;
console.log(Math.round(prix)); // arrondi au plus proche
console.log(Math.floor(prix)); // arrondi inférieur (« plancher »)
console.log(Math.ceil(prix)); // arrondi supérieur (« plafond »)
console.log(Math.trunc(prix)); // supprime la partie décimale
console.log(Math.min(89, 74, 112)); // le plus petit
console.log(Math.max(89, 74, 112)); // le plus grand
console.log(Math.abs(-15)); // valeur absolue
console.log(Math.sqrt(144)); // racine carrée
console.log(Math.PI);
// Arrondir à 2 décimales : on multiplie, on arrondit, on divise
const moyenne = 47.16666;
console.log(Math.round(moyenne * 100) / 100);
// Combien de lots de 6 vis faut-il pour en avoir 40 ?
console.log(Math.ceil(40 / 6));
Math.random() renvoie un nombre au hasard, entre
0 (inclus) et 1 (exclu). Pour obtenir un
entier entre deux bornes, on utilise une formule à
connaître par cœur. Relance l'exemple plusieurs fois :
console.log(Math.random());
// Entier entre min et max, bornes incluses
const min = 1;
const max = 6;
const de = Math.floor(Math.random() * (max - min + 1)) + min;
console.log("Lancer de dé :", de);
Deux valeurs spéciales font partie du type number :
console.log(10 / 0); // Infinity : l'infini
console.log(-10 / 0); // -Infinity
console.log("SSD" * 2); // NaN : « Not a Number », un calcul impossible
console.log(typeof NaN); // ironie : NaN est de type number
console.log(NaN === NaN); // NaN n'est égal à rien, pas même à lui-même !
console.log(Number.isNaN("SSD" * 2)); // la bonne façon de le détecter
NaN est contagieux : tout calcul qui l'implique donne
NaN. Si un total affiche NaN, remonte les calculs
un par un avec console.log : une des valeurs de départ
n'était pas un nombre (souvent un texte venu d'un formulaire).
Tout ce que tape un visiteur (formulaire, prompt()…) arrive
sous forme de texte. Avant de calculer, il faut le
convertir. Plusieurs outils existent, avec des comportements différents :
console.log(Number("42")); // conversion stricte
console.log(Number("42 €")); // le moindre caractère parasite : NaN
console.log(Number("")); // attention : une chaîne vide donne 0
console.log(parseInt("42 €")); // lit l'entier du début, ignore la suite
console.log(parseFloat("19.90 €")); // idem, avec les décimales
console.log(parseFloat("19,90")); // s'arrête à la virgule française !
Dans l'autre sens, pour transformer un nombre en texte :
const quantite = 3;
console.log(String(quantite), typeof String(quantite));
const prix = 89.9;
console.log(prix.toFixed(2)); // texte avec exactement 2 décimales
Et parfois, JavaScript convertit tout seul, sans qu'on le
lui demande. C'est la conversion implicite, et elle a une règle
piégeuse : avec +, le texte l'emporte (concaténation) ; avec
les autres opérateurs, le nombre l'emporte.
console.log("5" + 1); // "51" : le + colle
console.log("5" - 1); // 4 : le - calcule
console.log("5" * "2"); // 10
console.log("Total : " + 20 + 10); // de gauche à droite : texte, puis texte
console.log("Total : " + (20 + 10)); // les parenthèses calculent d'abord
Number() dès qu'une valeur
arrive sous forme de texte, et vérifie le résultat avec
Number.isNaN().
Intl.NumberFormat met en forme un nombre selon les habitudes
d'un pays : virgule décimale, espace entre les milliers, symbole de la
devise au bon endroit. Plus besoin de bricoler :
const euros = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" });
console.log(euros.format(1249.9));
console.log(euros.format(20));
console.log(euros.format(0.1 + 0.2)); // arrondit proprement à 2 décimales
// Pour un nombre simple, sans devise
console.log((1500000).toLocaleString("fr-FR"));
Un number représente les entiers de façon exacte jusqu'à
Number.MAX_SAFE_INTEGER, soit environ 9 millions de
milliards. Au-delà, la précision se perd :
console.log(Number.MAX_SAFE_INTEGER);
console.log(9007199254740992 + 1); // le +1 disparaît !
// Avec BigInt (le n final), le calcul reste exact
console.log(9007199254740992n + 1n);
console.log(2n ** 100n);
// Mais on ne mélange pas les deux types
console.log(10n + 1);
En pratique, tu n'auras presque jamais besoin de BigInt :
il sert pour des identifiants très longs, de la cryptographie ou des
calculs scientifiques. Pour un devis, même très gros, number
suffit largement.
const PRIX_SSD = 8990; // en centimes
const TAUX_HORAIRE = 4500; // en centimes
const heures = 1.5;
const REMISE_FIDELITE = 0.10; // 10 %
const DEVIS_DEDUIT = 2000; // en centimes
const sousTotal = PRIX_SSD + heures * TAUX_HORAIRE;
const remise = Math.round(sousTotal * REMISE_FIDELITE);
const total = sousTotal - remise - DEVIS_DEDUIT;
const euros = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" });
console.log(`Sous-total : ${euros.format(sousTotal / 100)}`);
console.log(`Remise : -${euros.format(remise / 100)}`);
console.log(`Devis payé : -${euros.format(DEVIS_DEDUIT / 100)}`);
console.log(`À régler : ${euros.format(total / 100)}`);
console.log("TVA non applicable, art. 293 B du CGI");
Math.round() sur la remise garantit qu'on reste sur
un nombre entier de centimes, quel que soit le pourcentage.
Un opérateur est un symbole qui agit sur une ou plusieurs
valeurs pour en produire une nouvelle : + additionne,
> compare, = affecte. Tu en as déjà croisé
beaucoup ; ce chapitre les range par famille et révèle leurs subtilités.
| Opérateur | Rôle | Exemple | Résultat |
|---|---|---|---|
+ | addition | 45 + 20 | 65 |
- | soustraction | 65 - 20 | 45 |
* | multiplication | 45 * 2 | 90 |
/ | division | 90 / 4 | 22.5 |
% | reste de la division entière (modulo) | 17 % 5 | 2 |
** | puissance | 2 ** 10 | 1024 |
Le modulo % mérite un exemple : il donne
ce qui reste après avoir fait des paquets.
const vis = 17;
const PAR_SACHET = 5;
console.log("Sachets complets :", Math.floor(vis / PAR_SACHET));
console.log("Vis restantes :", vis % PAR_SACHET);
// Astuce classique : un nombre est pair si son reste par 2 vaut 0
console.log("42 est pair ?", 42 % 2 === 0);
Comme en mathématiques, la multiplication et la division passent avant l'addition et la soustraction. Les parenthèses imposent l'ordre voulu :
console.log(45 + 20 * 2); // 20 * 2 d'abord : 85
console.log((45 + 20) * 2); // la parenthèse d'abord : 130
++ ajoute 1 à une variable, -- retire 1. C'est
l'outil des compteurs.
let clientsDuJour = 0;
clientsDuJour++; // un client arrive
clientsDuJour++; // un deuxième
console.log(clientsDuJour);
let placesEnAtelier = 4;
placesEnAtelier--; // un PC déposé
console.log(placesEnAtelier);
n++), l'opérateur renvoie
l'ancienne valeur puis incrémente ; placé avant
(++n), il incrémente d'abord. La différence n'apparaît que si
on utilise le résultat dans la même instruction :
console.log(n++) affiche l'ancienne valeur. Pour rester
lisible, écris toujours n++ seul sur sa ligne.
Au-delà du simple =, les affectations
composées combinent un calcul et une affectation :
total += 20 est la version courte de
total = total + 20.
let panier = 0;
panier += 89.90; // un SSD
panier += 45; // une heure de main d'œuvre
console.log("Panier :", panier);
panier *= 0.9; // remise de 10 %
console.log("Après remise :", panier); // le piège du chapitre 07 : d'où les centimes !
let note = "Écran changé";
note += ", pâte thermique refaite"; // fonctionne aussi avec les chaînes
console.log(note);
| Écriture courte | Équivaut à |
|---|---|
a += b | a = a + b |
a -= b | a = a - b |
a *= b | a = a * b |
a /= b | a = a / b |
a %= b | a = a % b |
a **= b | a = a ** b |
Une comparaison renvoie toujours un booléen, true ou false :
const stock = 3;
console.log(stock === 3); // strictement égal
console.log(stock !== 0); // strictement différent
console.log(stock > 5); // supérieur
console.log(stock <= 3); // inférieur ou égal
Il existe aussi une égalité « souple », == (et son contraire
!=). Elle convertit les valeurs avant de les comparer… avec
des résultats parfois absurdes :
console.log("20" == 20); // true : le texte est converti
console.log("20" === 20); // false : types différents
console.log(0 == ""); // true (!)
console.log(0 == "0"); // true…
console.log("" == "0"); // … mais false ici : plus rien n'est logique
console.log(null == undefined); // true
console.log(null === undefined); // false
=== et !==.
L'égalité stricte compare la valeur et le type, sans conversion
cachée. Si les types diffèrent, convertis d'abord toi-même avec
Number() ou String().
Les chaînes se comparent aussi, dans l'ordre du dictionnaire… ou presque : caractère par caractère, selon leur code informatique. Les majuscules passent avant les minuscules, et les nombres écrits en texte ne se comparent pas comme des nombres :
console.log("Brest" < "Morlaix"); // true : B avant M
console.log("morlaix" < "Quimper"); // false : toute minuscule vient après les majuscules
console.log("10" < "9"); // true : « 1 » vient avant « 9 »
console.log(10 < 9); // false : là, ce sont des nombres
Ils combinent plusieurs conditions :
| Opérateur | Se lit | Vaut true si… |
|---|---|---|
a && b | a ET b | les deux sont vrais |
a || b | a OU b | au moins un des deux est vrai |
!a | NON a | a est faux (il inverse) |
const sousGarantie = true;
const pieceDisponible = false;
const aDomicile = true;
console.log(sousGarantie && pieceDisponible); // réparation immédiate possible ?
console.log(sousGarantie || pieceDisponible); // au moins une bonne nouvelle ?
console.log(!aDomicile); // intervention à l'atelier ?
Deux secrets à connaître. D'abord, ces opérateurs sont
paresseux : && s'arrête dès qu'il
trouve une valeur fausse, || dès qu'il trouve une valeur
vraie. Ensuite, ils ne renvoient pas forcément un booléen, mais
la valeur à laquelle ils se sont arrêtés. D'où une
astuce très répandue pour fournir une valeur par défaut :
const pseudoSaisi = "";
const pseudo = pseudoSaisi || "Visiteur"; // "" est falsy : on prend la suite
console.log(`Bonjour ${pseudo} !`);
const connecte = true;
console.log(connecte && "Accès à l'espace client"); // vrai : on prend la suite
L'astuce du || a un défaut : elle remplace
toutes les valeurs falsy, y compris 0 et
"", qui sont parfois des valeurs tout à fait valables.
L'opérateur ?? ne remplace que
null et undefined :
const stockSsd = 0; // vraiment zéro : rupture de stock
console.log(stockSsd || "inconnu"); // ✗ « inconnu » : 0 a été jugé falsy
console.log(stockSsd ?? "inconnu"); // ✓ 0 : c'est une vraie valeur
let delaiFournisseur; // pas encore renseigné (undefined)
console.log(delaiFournisseur ?? "à confirmer");
a ??= b donne la valeur b à a
seulement si a est null ou
undefined. Même principe avec ||= et
&&=. Pratique pour compléter une valeur manquante
sans écraser une valeur existante.
Le seul opérateur à trois morceaux :
condition ? siVrai : siFaux. Il choisit entre deux valeurs
selon une condition. Parfait pour un petit choix dans une phrase :
const stock = 0;
const etiquette = stock > 0 ? "En stock" : "Sur commande";
console.log(etiquette);
const pieces = 1;
console.log(`${pieces} pièce${pieces > 1 ? "s" : ""} à commander`);
a ? b : c ? d : e). Ça tient sur une ligne, mais plus
personne ne sait la lire. Dès qu'il y a plus de deux cas, on utilisera
if / else (partie 2).
Petite avance sur la partie 3 : un objet regroupe
plusieurs valeurs nommées, comme une fiche client. Quand une information
peut manquer, lire une propriété d'une propriété absente
provoque une erreur. L'opérateur ?. s'arrête en douceur et
renvoie undefined :
const client = { nom: "Mme Le Gall" }; // pas d'adresse renseignée
console.log(client.adresse?.ville); // undefined, sans erreur
console.log(client.adresse?.ville ?? "Adresse à compléter"); // combiné avec ??
console.log(client.adresse.ville); // sans ?. : erreur !
Quand plusieurs opérateurs se suivent, JavaScript applique un ordre de priorité. Voici l'essentiel, du plus prioritaire au moins prioritaire :
| Rang | Opérateurs |
|---|---|
| 1 | ( ) parenthèses, . et ?. |
| 2 | !, typeof, ++, -- |
| 3 | ** |
| 4 | *, /, % |
| 5 | +, - |
| 6 | <, >, <=, >= |
| 7 | ===, !== |
| 8 | && |
| 9 | ||, ?? |
| 10 | ? : ternaire |
| 11 | =, +=, -=… affectations |
?? avec && ou || sans
parenthèses.
eligible, puis affiche
« Remise de 10 % accordée » ou « Pas de remise cette fois » grâce à un
ternaire. Teste plusieurs combinaisons avec Modifier.
const interventions = 2;
const depenses = 640;
const dejaRemise = false;
// À toi d'écrire la suite…
const interventions = 2;
const depenses = 640;
const dejaRemise = false;
const eligible = (interventions >= 3 || depenses > 500) && !dejaRemise;
console.log("Éligible :", eligible);
console.log(eligible ? "Remise de 10 % accordée" : "Pas de remise cette fois");
Les parenthèses sont indispensables : sans elles,
&& passerait avant || et la
condition changerait de sens.
Bravo, les fondations sont posées ! 🎉 Tu sais maintenant :
defer ;const et let ;===, combiner avec &&, ||, ?? et choisir avec le ternaire.// Tout ce que tu sais faire, en quelques lignes
const CLIENT = "Yann Le Bihan";
const ssdEnStock = 0;
let totalCentimes = 8990 + 1.5 * 4500;
const euros = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" });
const disponibilite = ssdEnStock > 0 ? "pièce en stock" : "pièce sur commande";
console.log(`${CLIENT.toUpperCase()} — ${disponibilite}`);
console.log(`Total estimé : ${euros.format(totalCentimes / 100)}`);
Dans la partie 2, ton code va apprendre à décider (les conditions), à répéter (les boucles) et à se découper en outils réutilisables (les fonctions). C'est là que la programmation devient vraiment amusante.
const en let, remplace === par
==. Observer les erreurs et les comportements bizarres est la
meilleure façon d'apprendre à les reconnaître le jour où ils arriveront
pour de vrai.
Jusqu'ici, nos programmes exécutaient toutes leurs lignes, toujours dans le même ordre. Un vrai programme doit savoir choisir : « s'il reste des SSD, on monte la pièce tout de suite ; sinon, on la commande ». C'est le rôle des conditions.
if exécute un bloc de code seulement si la
condition entre parenthèses est vraie. Sinon, le bloc est sauté et le
programme continue après l'accolade fermante.
const ssdEnStock = 3;
if (ssdEnStock > 0) {
console.log("SSD disponible : montage aujourd'hui.");
}
console.log("Fin de la vérification."); // s'exécute dans tous les cas
Passe ssdEnStock à 0 avec Modifier et
relance : la première ligne disparaît, la seconde reste.
if (stock > 0) console.log("ok");, mais le jour où tu
ajoutes une deuxième ligne, elle s'exécutera en dehors de la
condition sans que rien ne le signale.
else ajoute un second bloc, exécuté quand la condition est fausse. Un seul des deux s'exécute, jamais les deux :
const ssdEnStock = 0;
if (ssdEnStock > 0) {
console.log("SSD disponible : montage aujourd'hui.");
} else {
console.log("Rupture : commande passée, livraison sous 48 h.");
}
Quand il y a plus de deux possibilités, on enchaîne des
else if. JavaScript teste les conditions
dans l'ordre et s'arrête à la première qui est vraie :
const ageDuPc = 7; // en années
if (ageDuPc < 3) {
console.log("PC récent : un nettoyage suffit sans doute.");
} else if (ageDuPc < 6) {
console.log("PC dans la force de l'âge : un SSD lui redonnera un coup de jeune.");
} else if (ageDuPc < 10) {
console.log("PC âgé : comparons le coût de la réparation avec un neuf.");
} else {
console.log("PC de collection : place à un nouveau montage !");
}
if (ageDuPc < 10), un PC de 2 ans entrerait dans ce
premier bloc et ne serait jamais reconnu comme « récent ». Range les
conditions du cas le plus précis au cas le plus large, ou dans un ordre
croissant comme ici.
Les opérateurs logiques du chapitre 08 prennent ici tout leur sens :
const sousGarantie = false;
const pieceDisponible = true;
const accordClient = true;
if (sousGarantie || (pieceDisponible && accordClient)) {
console.log("On lance la réparation.");
} else {
console.log("On attend : garantie, pièce ou accord manquant.");
}
if (!pieceDisponible) {
console.log("Penser à commander la pièce.");
}
La condition d'un if n'a pas besoin d'être un booléen :
JavaScript convertit la valeur selon la règle des valeurs falsy vue au
chapitre 05. C'est pratique pour vérifier qu'un champ
a été rempli :
const telephone = ""; // champ laissé vide
if (telephone) {
console.log(`On vous rappelle au ${telephone}.`);
} else {
console.log("Merci d'indiquer un numéro de téléphone.");
}
0 comme « absent ». Avec
const remise = 0;, le test if (remise) est faux
alors que la remise est bien renseignée (elle vaut zéro). Quand
0 est une valeur possible, teste explicitement :
if (remise !== undefined) ou utilise ??.
On peut placer un if dans un autre, mais au-delà de deux
niveaux le code devient illisible. Souvent, une condition combinée dit la
même chose bien plus clairement :
// ✗ Imbriqué : il faut suivre trois niveaux d'accolades
if (clientConnu) {
if (interventions >= 3) {
if (!dejaRemise) {
console.log("Remise fidélité !");
}
}
}
// ✓ À plat : une seule condition, qui se lit comme une phrase
if (clientConnu && interventions >= 3 && !dejaRemise) {
console.log("Remise fidélité !");
}
Au chapitre 11, tu découvriras une autre arme contre l'imbrication : le retour anticipé.
Quand on compare une même valeur à une liste de
possibilités exactes, switch est plus lisible qu'une
cascade de else if :
const statut = "attente-piece";
switch (statut) {
case "recu":
console.log("📥 Appareil reçu, diagnostic à venir.");
break;
case "diagnostic":
console.log("🔍 Diagnostic en cours.");
break;
case "attente-piece":
console.log("📦 En attente d'une pièce.");
break;
case "pret":
console.log("✅ Prêt à être récupéré.");
break;
default:
console.log("❓ Statut inconnu.");
}
Le fonctionnement :
case, avec une égalité stricte (===) ;case qui correspond, le code s'exécute jusqu'au prochain break ;default (facultatif) s'exécute si aucun cas ne correspond, comme le else final.break, l'exécution « tombe » dans les cas suivants,
qu'ils correspondent ou non. Regarde :
const statut = "diagnostic";
switch (statut) {
case "recu":
console.log("Appareil reçu.");
case "diagnostic":
console.log("Diagnostic en cours."); // ✓ attendu
case "pret":
console.log("Prêt à être récupéré."); // ✗ affiché quand même !
}
Ce comportement a pourtant un usage légitime : regrouper plusieurs cas qui partagent le même traitement, en les empilant :
const jour = "samedi";
switch (jour) {
case "lundi":
case "mardi":
case "mercredi":
case "jeudi":
case "vendredi":
console.log("Ouvert de 9 h à 12 h et de 14 h à 18 h.");
break;
case "samedi":
console.log("Ouvert de 9 h à 12 h.");
break;
default:
console.log("Fermé le dimanche.");
}
| Situation | Outil |
|---|---|
| choisir entre deux valeurs, dans une expression | ternaire ? : |
| exécuter ou non un bloc | if |
deux chemins, ou des conditions variées (<, &&…) | if / else if / else |
| une même valeur comparée à une liste de valeurs exactes | switch |
jour et heure (un nombre entier, 14
pour 14 h), affiche « Ouvert » ou « Fermé ». Teste au moins : mardi 10 h,
mardi 12 h, samedi 15 h, dimanche 10 h.
const jour = "mardi";
const heure = 10;
// À toi d'écrire la suite…
const jour = "mardi";
const heure = 10;
const matin = heure >= 9 && heure < 12;
const apresMidi = heure >= 14 && heure < 18;
let ouvert;
switch (jour) {
case "lundi":
case "mardi":
case "mercredi":
case "jeudi":
case "vendredi":
ouvert = matin || apresMidi;
break;
case "samedi":
ouvert = matin;
break;
default:
ouvert = false;
}
console.log(`${jour} ${heure} h : ${ouvert ? "Ouvert" : "Fermé"}`);
Ranger les plages horaires dans des constantes au nom parlant
(matin, apresMidi) rend le
switch presque lisible à voix haute. À 12 h pile,
heure < 12 est faux : l'atelier est bien fermé.
Afficher les créneaux d'une journée, additionner les lignes d'une facture, vérifier chaque pièce d'une commande : beaucoup de tâches consistent à répéter la même action. Plutôt que de copier-coller dix fois la même ligne, on écrit une boucle.
for est la boucle à utiliser quand on sait
combien de fois répéter. Sa parenthèse contient trois
morceaux séparés par des points-virgules :
for (let poste = 1; poste <= 4; poste++) {
console.log(`Poste de travail n° ${poste} : prêt`);
}
| Morceau | Dans l'exemple | Quand ? |
|---|---|---|
| Initialisation | let poste = 1 | une seule fois, avant de commencer |
| Condition | poste <= 4 | avant chaque tour : si elle est fausse, la boucle s'arrête |
| Mise à jour | poste++ | à la fin de chaque tour |
Chaque passage dans le bloc s'appelle une itération (un « tour »). Rien n'oblige à compter de 1 en 1 : on peut partir de n'importe où et avancer à n'importe quel pas.
// Créneaux de rendez-vous toutes les 30 minutes, de 9 h à 11 h 30
for (let minutes = 9 * 60; minutes <= 11 * 60 + 30; minutes += 30) {
const h = Math.floor(minutes / 60);
const m = String(minutes % 60).padStart(2, "0");
console.log(`${h} h ${m}`);
}
// Compte à rebours
for (let i = 3; i > 0; i--) {
console.log(`Redémarrage dans ${i}…`);
}
while (« tant que ») répète un bloc
tant que sa condition reste vraie. On l'utilise quand on
ne sait pas à l'avance combien de tours seront nécessaires :
// Combien de mois pour s'offrir un PC à 1 100 € en mettant 120 € de côté par mois ?
const PRIX_PC = 1100;
const EPARGNE_MENSUELLE = 120;
let cagnotte = 0;
let mois = 0;
while (cagnotte < PRIX_PC) {
cagnotte += EPARGNE_MENSUELLE;
mois++;
}
console.log(`Il faut ${mois} mois (cagnotte : ${cagnotte} €).`);
Variante de while où la condition est vérifiée
après le bloc : celui-ci s'exécute donc
au moins une fois. Relance plusieurs fois l'exemple :
// On relance le test de mémoire jusqu'à ce qu'il réussisse (au hasard ici)
let tentative = 0;
let reussi;
do {
tentative++;
reussi = Math.random() > 0.6;
console.log(`Test n° ${tentative} : ${reussi ? "OK" : "échec"}`);
} while (!reussi);
let stock = 5;
while (stock > 0) {
console.log("Il reste du stock"); // stock vaut toujours 5… pour l'éternité
}
Le réflexe : dans toute boucle while, vérifie qu'au moins une
ligne du bloc fait évoluer la condition vers sa fin. Et si
ça t'arrive quand même, pas de panique : ferme l'onglet (ou le
navigateur te proposera d'arrêter le script).
Deux mots-clés modifient le déroulement d'une boucle :
break la quitte immédiatement,
continue passe directement au tour suivant.
// continue : on saute la pause déjeuner
for (let heure = 9; heure < 18; heure++) {
if (heure === 12 || heure === 13) {
continue;
}
console.log(`${heure} h : créneau disponible`);
}
// break : on cherche le premier numéro de ticket divisible par 7, puis on s'arrête
for (let ticket = 40; ticket < 100; ticket++) {
if (ticket % 7 === 0) {
console.log(`Ticket gagnant : ${ticket}`);
break;
}
}
Les boucles brillent vraiment avec les tableaux, ces
listes de valeurs entre crochets qu'on étudiera en détail dans la
partie 3. Pour l'instant, il suffit de savoir qu'un tableau numérote ses
éléments à partir de 0, comme les caractères d'une chaîne, et
qu'il a lui aussi une propriété length :
const pieces = ["SSD 1 To", "RAM 16 Go", "Ventirad"];
console.log(pieces.length); // 3 éléments
console.log(pieces[0]); // le premier
for (let i = 0; i < pieces.length; i++) {
console.log(`Pièce ${i + 1} : ${pieces[i]}`);
}
Parcourir un tableau avec un compteur i fonctionne, mais
c'est verbeux et propice aux erreurs (<= au lieu de
<, et l'on sort du tableau). for…of donne
directement chaque élément, l'un après l'autre :
const pieces = ["SSD 1 To", "RAM 16 Go", "Ventirad"];
for (const piece of pieces) {
console.log(`À commander : ${piece}`);
}
// Ça marche aussi avec une chaîne, caractère par caractère
for (const lettre of "AI29") {
console.log(lettre);
}
const dans un for…of : à chaque
tour, une nouvelle variable est créée avec l'élément suivant. Dans
un for classique, en revanche, le compteur change de valeur :
il faut let.
for…in parcourt les noms des propriétés
d'un objet (encore une avance sur la partie 3) :
const fiche = { client: "Mme Le Gall", appareil: "PC portable", panne: "écran noir" };
for (const cle in fiche) {
console.log(`${cle} → ${fiche[cle]}`);
}
for…of pour les
valeurs d'une liste (« chaque pièce of la commande »),
for…in pour les clés d'un objet. N'utilise
pas for…in sur un tableau : il renvoie les positions sous forme
de texte ("0", "1"…), ce qui provoque
des bugs sournois.
Une boucle peut en contenir une autre. La boucle intérieure fait alors tous ses tours à chaque tour de la boucle extérieure :
const jours = ["lundi", "mardi", "mercredi"];
const creneaux = ["9 h", "14 h", "16 h"];
for (const jour of jours) {
let ligne = `${jour.padEnd(9)}:`;
for (const creneau of creneaux) {
ligne += ` ${creneau}`;
}
console.log(ligne);
}
| Besoin | Boucle |
|---|---|
| parcourir les éléments d'un tableau ou d'une chaîne | for…of (le plus courant) |
| répéter un nombre connu de fois, ou avoir besoin de la position | for |
| répéter tant qu'une situation dure, sans savoir combien de fois | while |
| idem, mais au moins une fois | do…while |
| parcourir les propriétés d'un objet | for…in |
const montants = [45, 129.9, 20, 310, 89.9, 65, 0, 154];
// À toi d'écrire la suite…
const montants = [45, 129.9, 20, 310, 89.9, 65, 0, 154];
let total = 0;
let grosses = 0;
let maximum = 0;
for (const montant of montants) {
total += montant;
if (montant > 100) {
grosses++;
}
if (montant > maximum) {
maximum = montant;
}
}
console.log(`Total du mois : ${total.toFixed(2)} €`);
console.log(`Interventions > 100 € : ${grosses}`);
console.log(`Plus grosse intervention : ${maximum} €`);
Le 0 correspond à un diagnostic gratuit : il compte
bien comme une intervention. Pour le maximum, on aurait aussi pu
écrire Math.max(...montants), une syntaxe qu'on
verra au chapitre suivant.
À l'atelier, chaque opération courante a sa fiche de procédure : « remplacer un SSD », « changer la pâte thermique ». On l'écrit une fois, soigneusement, puis on la suit à chaque fois qu'il le faut, sans tout réinventer. Une fonction, c'est exactement ça : un bloc de code nommé, écrit une fois et réutilisable à volonté.
Les fonctions apportent trois bénéfices majeurs :
calculerTotal(lignes) se comprend sans lire son contenu ;// Déclaration : on écrit la procédure (rien ne s'exécute encore)
function afficherHoraires() {
console.log("Lundi – vendredi : 9 h – 12 h / 14 h – 18 h");
console.log("Samedi : 9 h – 12 h");
}
// Appel : on exécute la procédure, autant de fois qu'on veut
afficherHoraires();
console.log("---");
afficherHoraires();
La déclaration commence par le mot-clé function, suivi du
nom, de parenthèses et d'un bloc. Pour appeler la
fonction, on écrit son nom suivi de parenthèses. Sans les parenthèses, on
ne l'exécute pas : on désigne simplement la fonction elle-même.
function direBonjour() {
console.log("Bonjour !");
}
console.log(direBonjour); // la fonction elle-même (pas exécutée)
direBonjour(); // l'appel
Une fiche de procédure « remplacer un composant » est plus utile si on peut préciser quel composant. Les paramètres sont les variables qui reçoivent les informations au moment de l'appel. Les valeurs transmises s'appellent les arguments.
function accueillir(client, appareil) { // paramètres
console.log(`Bonjour ${client}, déposez votre ${appareil} sur le comptoir.`);
}
accueillir("Mme Le Gall", "PC portable"); // arguments
accueillir("M. Kerboriou", "unité centrale");
accueillir("Yann"); // argument manquant…
Un argument manquant vaut undefined, d'où le texte bancal de
la dernière ligne. Pour l'éviter, on donne une valeur par
défaut au paramètre avec = :
function accueillir(client, appareil = "appareil") {
console.log(`Bonjour ${client}, déposez votre ${appareil} sur le comptoir.`);
}
accueillir("Yann");
accueillir("Yann", "smartphone");
Une fonction peut renvoyer une valeur avec
return. L'appel de la fonction est alors remplacé
par cette valeur, qu'on peut ranger dans une variable, afficher ou
réutiliser dans un autre calcul.
function prixMainOeuvre(heures, tauxHoraire = 45) {
return heures * tauxHoraire;
}
const montage = prixMainOeuvre(2);
const reparation = prixMainOeuvre(1.5, 50);
console.log(montage, reparation);
console.log("Total :", montage + reparation);
console.log("Avec une pièce :", prixMainOeuvre(1) + 89.9);
return termine immédiatement la fonction :
ce qui suit ne s'exécute pas. Et une fonction sans return
renvoie undefined.
console.log() affiche une valeur pour toi, le
développeur ; return la transmet au reste du
programme. Une fonction de calcul doit renvoyer son résultat, sinon on ne
peut rien en faire :
function totalQuiAffiche(a, b) {
console.log(a + b); // affiche… mais ne renvoie rien
}
function totalQuiRenvoie(a, b) {
return a + b;
}
const x = totalQuiAffiche(45, 20);
const y = totalQuiRenvoie(45, 20);
console.log("x vaut", x, "— y vaut", y);
Comme return arrête la fonction, on peut traiter les cas
particuliers en premier et sortir aussitôt. Le cas
normal se retrouve à plat, en fin de fonction, sans imbrication :
function prixDevis(montantIntervention, devisValide) {
if (montantIntervention <= 0) {
return "Diagnostic seul : gratuit";
}
if (!devisValide) {
return "Devis refusé : 20 €";
}
return `Intervention : ${montantIntervention - 20} € (devis déduit)`;
}
console.log(prixDevis(0, false));
console.log(prixDevis(150, false));
console.log(prixDevis(150, true));
Une fonction est un bloc : les variables déclarées à l'intérieur (y compris ses paramètres) sont locales, invisibles de l'extérieur. En revanche, une fonction voit les variables déclarées autour d'elle.
const ATELIER = "AI29"; // déclarée dehors : visible partout
function signature(technicien) {
const ville = "Morlaix"; // locale : n'existe que dans la fonction
return `${technicien} — ${ATELIER}, ${ville}`;
}
console.log(signature("Shooby"));
console.log(ville); // erreur : ville n'existe pas ici
En JavaScript, une fonction est une valeur, au même titre qu'un nombre ou une chaîne. On peut donc la ranger dans une variable. On parle alors d'expression de fonction, et la fonction n'a souvent pas de nom propre : elle est anonyme.
const appliquerRemise = function (prix, pourcentage) {
return prix * (1 - pourcentage / 100);
};
console.log(appliquerRemise(200, 10));
console.log(typeof appliquerRemise);
Différence importante avec une déclaration classique : une fonction
déclarée avec function nom() est
« hissée », on peut l'appeler avant même la ligne où elle est écrite.
Une expression de fonction rangée dans un const suit les
règles de const : pas d'accès avant sa déclaration.
console.log(tarifDeclare()); // ✓ fonctionne : la déclaration est hissée
function tarifDeclare() {
return "20 €";
}
console.log(tarifExprime()); // ✗ erreur : const pas encore initialisé
const tarifExprime = function () {
return "20 €";
};
Depuis 2015, une écriture plus compacte existe : la fonction
fléchée, avec le symbole =>. Elle est
omniprésente dans le code moderne.
// Version complète, avec un bloc et return
const prixTotal = (prix, quantite) => {
return prix * quantite;
};
// Une seule expression : on retire accolades et return (le retour est implicite)
const prixTotalCourt = (prix, quantite) => prix * quantite;
// Un seul paramètre : les parenthèses deviennent facultatives
const enMajuscules = texte => texte.toUpperCase();
// Aucun paramètre : parenthèses vides obligatoires
const salutation = () => "Bienvenue à l'atelier !";
console.log(prixTotal(89.9, 2), prixTotalCourt(89.9, 2));
console.log(enMajuscules("morlaix"));
console.log(salutation());
() => ({ nom: "SSD" }). Sans elles, les
accolades sont prises pour un bloc de code, et la fonction renvoie
undefined. Autre différence, plus subtile : les fonctions
fléchées n'ont pas leur propre this. On y reviendra avec les
objets.
| Forme | Écriture | Hissée ? |
|---|---|---|
| déclaration | function double(n) { return n * 2; } | oui |
| expression | const double = function (n) { return n * 2; }; | non |
| fléchée | const double = n => n * 2; | non |
Puisqu'une fonction est une valeur, on peut la donner en argument à une autre fonction, qui l'appellera au bon moment. Une fonction transmise ainsi s'appelle un callback (« rappel »). C'est l'un des mécanismes les plus importants de JavaScript.
function traiterCommande(pieces, action) {
for (const piece of pieces) {
action(piece); // on appelle le callback pour chaque pièce
}
}
const commande = ["SSD 1 To", "RAM 16 Go"];
traiterCommande(commande, piece => console.log(`Réceptionner : ${piece}`));
traiterCommande(commande, piece => console.log(`Étiqueter : ${piece.toUpperCase()}`));
La même fonction traiterCommande fait deux choses
différentes selon le callback qu'on lui confie. Les tableaux possèdent
d'ailleurs une méthode toute faite sur ce principe,
forEach(), qui appelle le callback pour chaque élément en
lui donnant aussi sa position :
const etapes = ["Diagnostic", "Devis détaillé", "Intervention"];
etapes.forEach((etape, index) => {
console.log(`Étape ${index + 1} : ${etape}`);
});
Autre callback célèbre : setTimeout(callback, délai) appelle
la fonction après un délai en millisecondes. Observe l'ordre des messages
(la dernière ligne s'affiche au bout d'une seconde) :
console.log("1. Le PC est lancé en test de chauffe.");
setTimeout(() => {
console.log("3. Une seconde plus tard : test terminé, températures OK.");
}, 1000);
console.log("2. Pendant ce temps, on prépare la facture.");
Le programme n'attend pas : il programme le callback, poursuit son chemin, et la fonction est rappelée plus tard. C'est le principe de l'asynchrone, auquel la partie 5 est consacrée.
Les trois points ... devant le dernier paramètre (le
paramètre du reste) rassemblent tous les arguments restants
dans un tableau. La fonction accepte alors autant d'arguments qu'on veut :
function totalFacture(client, ...montants) {
let total = 0;
for (const montant of montants) {
total += montant;
}
return `${client} : ${montants.length} ligne(s), ${total} €`;
}
console.log(totalFacture("Mme Le Gall", 89.9));
console.log(totalFacture("M. Kerboriou", 45, 20, 129.9));
À l'inverse, au moment de l'appel, les trois points « étalent » un tableau en arguments séparés. C'est la syntaxe de décomposition (spread) :
const devis = [129.9, 310, 89.9, 154];
console.log(Math.max(devis)); // NaN : Math.max attend des nombres, pas un tableau
console.log(Math.max(...devis)); // équivaut à Math.max(129.9, 310, 89.9, 154)
Une fonction se souvient des variables qui l'entouraient au moment de sa création, même quand on l'utilise bien plus tard, ailleurs. Ce mécanisme s'appelle une fermeture (closure). Il permet par exemple de fabriquer un distributeur de numéros de ticket dont personne ne peut trafiquer le compteur :
function creerDistributeurTickets(dernierNumero) {
let numero = dernierNumero; // variable privée du distributeur
return () => {
numero++;
return `R-${String(numero).padStart(4, "0")}`;
};
}
const prochainTicket = creerDistributeurTickets(41);
console.log(prochainTicket());
console.log(prochainTicket());
console.log(prochainTicket());
console.log(typeof numero); // inaccessible de l'extérieur
(() => { … })();, une fonction fléchée appelée aussitôt
créée. Grâce à la portée, ses variables restent privées et ne risquent
pas d'entrer en collision avec celles d'un autre script. Tu sais
maintenant lire cette ligne !
calculerTotal, formaterPrix, envoyerDevis. Pour une fonction qui répond par oui ou non : estOuvert, aUneGarantie.estOuvert(jour, heure) qui
renvoie true ou false. Écris
aussi une fonction fléchée formaterEuros(montant) qui
renvoie le montant au format français (chapitre 07). Enfin, avec une
boucle, teste estOuvert sur plusieurs couples jour / heure.
function estOuvert(jour, heure) {
const matin = heure >= 9 && heure < 12;
const apresMidi = heure >= 14 && heure < 18;
if (jour === "dimanche") {
return false;
}
if (jour === "samedi") {
return matin;
}
return matin || apresMidi;
}
const euros = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" });
const formaterEuros = montant => euros.format(montant);
const tests = [["mardi", 10], ["mardi", 12], ["samedi", 11], ["samedi", 15], ["dimanche", 10]];
for (const [jour, heure] of tests) {
console.log(`${jour} ${heure} h →`, estOuvert(jour, heure) ? "Ouvert" : "Fermé");
}
console.log(formaterEuros(129.9));
Le retour anticipé remplace le switch : le cas du
dimanche, puis celui du samedi, sont réglés d'entrée, et la
dernière ligne couvre la semaine. L'écriture
const [jour, heure] of tests range directement les
deux valeurs de chaque petit tableau dans deux variables : c'est
la déstructuration, au programme de la partie 3.
Ton code sait maintenant réfléchir et travailler à la chaîne ! 🎉 Tu sais :
if, else if, else et switch, sans tomber dans le piège du break oublié ;for, while, do…while, et la piloter avec break et continue ;for…of et un objet avec for…in ;return ;// Tout ce que tu sais faire, en quelques lignes
const euros = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" });
function totalLignes(...montants) {
let total = 0;
for (const montant of montants) {
total += montant;
}
return total;
}
const remiseFidelite = (total, interventions) => (interventions >= 3 ? total * 0.1 : 0);
const total = totalLignes(89.9, 45, 20);
const remise = remiseFidelite(total, 4);
console.log(`Total : ${euros.format(total)}`);
if (remise > 0) {
console.log(`Remise fidélité : -${euros.format(remise)}`);
}
console.log(`À régler : ${euros.format(total - remise)}`);
Dans la partie 3, place aux données :
les tableaux et leurs méthodes magiques (map,
filter, reduce), les objets pour décrire un
client ou une réparation, puis les classes pour fabriquer des objets en
série.
Une variable contient une valeur. Mais une commande contient plusieurs pièces, un planning plusieurs rendez-vous, une facture plusieurs lignes. Pour ranger une liste ordonnée de valeurs sous un seul nom, on utilise un tableau (array). Tu en as eu un aperçu dans la partie 2 : place au mode d'emploi complet.
const pieces = ["SSD 1 To", "RAM 16 Go", "Ventirad"];
const prix = [89.9, 54, 32.5];
const melange = ["Morlaix", 29, true, null]; // possible, mais rarement une bonne idée
const vide = [];
console.log(pieces);
console.log(prix.length, "prix");
console.log(vide.length);
console.log(Array.isArray(pieces), typeof pieces);
Les éléments s'écrivent entre crochets, séparés par des
virgules. Comme le montre la dernière ligne, un tableau est un
objet (typeof répond
"object") : pour reconnaître un tableau à coup sûr, on utilise
Array.isArray().
const planning = ["Diagnostic PC", "Montage tour", "Nettoyage portable"];
console.log(planning[0]); // premier élément : position 0
console.log(planning.at(-1)); // dernier élément
console.log(planning[10]); // hors du tableau : undefined
planning[1] = "Montage tour gamer"; // remplacer un élément
console.log(planning);
const, et ça
marche ! const interdit de remplacer le tableau par un
autre (planning = […]), pas d'en changer le
contenu. Pense à un bac de rangement fixé au mur : on ne peut pas
l'échanger contre un autre bac, mais on peut y ajouter ou en retirer des
pièces. C'est vrai pour tous les objets.
const fileAttente = ["PC de Mme Le Gall", "Portable de Yann"];
fileAttente.push("Tour de M. Kerboriou"); // ajoute à la fin
console.log(fileAttente);
fileAttente.unshift("Urgence : serveur"); // ajoute au début
console.log(fileAttente);
const termine = fileAttente.shift(); // retire le premier et le renvoie
console.log("Terminé :", termine);
const annule = fileAttente.pop(); // retire le dernier et le renvoie
console.log("Annulé :", annule);
console.log(fileAttente);
push / pop travaillent à la fin
(comme une pile d'assiettes) ; unshift / shift
travaillent au début. Une file d'attente « premier arrivé,
premier servi » se gère avec push pour entrer et
shift pour sortir.
splice(position, nombreÀRetirer, …élémentsÀInsérer) est le
couteau suisse pour retirer ou insérer n'importe où. Il modifie le tableau
et renvoie les éléments retirés :
const etapes = ["Démontage", "Nettoyage", "Remontage", "Tests"];
const retirees = etapes.splice(1, 1); // retire 1 élément en position 1
console.log(retirees, etapes);
etapes.splice(1, 0, "Changement de pâte thermique"); // insère sans rien retirer
console.log(etapes);
etapes.splice(2, 1, "Remontage soigné"); // remplace 1 élément
console.log(etapes);
const marques = ["Asus", "Lenovo", "HP", "Acer", "HP"];
console.log(marques.includes("Lenovo")); // présent ?
console.log(marques.indexOf("HP")); // première position
console.log(marques.lastIndexOf("HP")); // dernière position
console.log(marques.indexOf("Dell")); // -1 : absent
Pour chercher selon une condition plutôt qu'une valeur
exacte, on passe un callback (chapitre 11) qui renvoie
true pour l'élément recherché :
const prix = [89.9, 54, 32.5, 129, 15];
console.log(prix.find(p => p > 100)); // premier élément qui correspond
console.log(prix.findIndex(p => p > 100)); // sa position
console.log(prix.find(p => p > 500)); // aucun : undefined
console.log(prix.some(p => p < 20)); // au moins un ?
console.log(prix.every(p => p > 10)); // tous ?
Au chapitre 05, copier un primitif créait une copie indépendante. Avec un tableau (et tout objet), c'est différent : la variable ne contient pas le tableau lui-même, mais une référence vers lui, comme une adresse. Copier la variable copie l'adresse, pas le contenu :
const commandePrevue = ["SSD", "RAM"];
const commandeFinale = commandePrevue; // même tableau, deux noms !
commandeFinale.push("Alimentation");
console.log(commandePrevue); // modifié lui aussi…
console.log(commandePrevue === commandeFinale);
Pour obtenir une vraie copie, on crée un nouveau tableau avec la syntaxe de décomposition ... vue au chapitre 11 :
const commandePrevue = ["SSD", "RAM"];
const commandeFinale = [...commandePrevue]; // nouveau tableau, mêmes éléments
commandeFinale.push("Alimentation");
console.log(commandePrevue, commandeFinale);
// Fusionner plusieurs tableaux
const stockAtelier = ["Vis", "Pâte thermique"];
const tout = [...commandeFinale, ...stockAtelier, "Câble SATA"];
console.log(tout);
Ces trois méthodes remplacent la plupart des boucles qu'on écrirait à la main. Elles prennent un callback, ne modifient pas le tableau d'origine et renvoient un résultat neuf.
map() transforme chaque élément et renvoie un tableau de même longueur :
const prixEnEuros = [89.9, 54, 32.5];
const prixEnCentimes = prixEnEuros.map(prix => Math.round(prix * 100));
const etiquettes = prixEnEuros.map((prix, i) => `Article ${i + 1} : ${prix} €`);
console.log(prixEnCentimes);
console.log(etiquettes);
filter() garde les éléments pour lesquels le callback renvoie true :
const prix = [89.9, 54, 32.5, 129, 15];
const petitsPrix = prix.filter(p => p < 50);
console.log(petitsPrix);
console.log(prix); // l'original est intact
reduce() réduit tout le tableau à une seule
valeur. Le callback reçoit un accumulateur (le résultat en
cours) et l'élément courant ; le second argument de reduce
est la valeur de départ de l'accumulateur :
const lignesFacture = [89.9, 54, 45];
const total = lignesFacture.reduce((somme, montant) => somme + montant, 0);
console.log(`Total : ${total} €`);
// Tour par tour : somme = 0 → 89.9 → 143.9 → 188.9
Leur vraie force : elles s'enchaînent, comme une chaîne de montage.
const prixHT = [89.9, 54, 32.5, 129, 15];
// Total des articles à plus de 30 €, après remise de 10 %
const total = prixHT
.filter(p => p > 30) // garde : 89.9, 54, 32.5, 129
.map(p => p * 0.9) // remise sur chacun
.reduce((s, p) => s + p, 0); // additionne
console.log(total.toFixed(2));
const villes = ["Morlaix", "Brest", "Landivisiau", "Carhaix"];
villes.sort();
console.log(villes);
const prix = [89, 129, 15, 54, 1000];
prix.sort();
console.log(prix); // 😱 1000 avant 129 ?!
sort() trie tout comme du texte,
même les nombres : « 1000 » commence par « 1 », il passe donc avant
« 129 ». Pour trier des nombres, on fournit une fonction de comparaison :
elle reçoit deux éléments a et b et renvoie un
nombre négatif si a doit passer avant, positif sinon.
const prix = [89, 129, 15, 54, 1000];
prix.sort((a, b) => a - b); // croissant
console.log(prix);
prix.sort((a, b) => b - a); // décroissant
console.log(prix);
// Texte avec accents, à la française
const clients = ["Émilie", "Yann", "Erwan", "Agathe"];
clients.sort((a, b) => a.localeCompare(b, "fr"));
console.log(clients);
Attention, sort() modifie le tableau d'origine. La version
récente toSorted() renvoie une copie triée et laisse
l'original intact. De même, reverse() inverse le tableau sur
place, et toReversed() renvoie une copie inversée.
const arrivees = ["Mme Le Gall", "Yann", "M. Kerboriou"];
const parOrdreAlpha = arrivees.toSorted((a, b) => a.localeCompare(b, "fr"));
console.log(arrivees); // ordre d'arrivée conservé
console.log(parOrdreAlpha);
const pieces = ["SSD", "RAM", "GPU"];
console.log(pieces.join(", ")); // tableau → texte
console.log("Lun;Mar;Mer".split(";")); // texte → tableau
console.log(pieces.slice(0, 2)); // copie d'une portion (sans modifier)
const parKit = [["Vis", "Entretoises"], ["Pâte thermique"]];
console.log(parKit.flat()); // aplatit d'un niveau
console.log(Array.from({ length: 5 }, (_, i) => `Poste ${i + 1}`)); // génère un tableau
console.log(new Array(3).fill("libre"));
La déstructuration range les éléments d'un tableau dans des variables, en une seule ligne, selon leur position :
const creneau = ["mardi", "14 h", "Mme Le Gall"];
const [jour, heure, client] = creneau;
console.log(`${client} passe ${jour} à ${heure}.`);
const [premier, , troisieme] = ["SSD", "RAM", "GPU"]; // on saute un élément
console.log(premier, troisieme);
const [gagnant, ...autres] = ["Yann", "Erwan", "Agathe"]; // le reste dans un tableau
console.log(gagnant, autres);
let a = "écran", b = "clavier";
[a, b] = [b, a]; // échanger deux variables
console.log(a, b);
| Modifient l'original | Renvoient du neuf, original intact |
|---|---|
push, pop, shift, unshift | map, filter, reduce |
splice | slice, concat, [...tableau] |
sort, reverse | toSorted, toReversed |
fill | find, includes, join, flat |
[nom, prix] avec map.
const noms = ["SSD 1 To", "Carte graphique", "Ventirad", "RAM 16 Go", "Boîtier"];
const prix = [89.9, 459, 32.5, 54, 119];
// À toi d'écrire la suite…
const noms = ["SSD 1 To", "Carte graphique", "Ventirad", "RAM 16 Go", "Boîtier"];
const prix = [89.9, 459, 32.5, 54, 119];
const abordables = noms
.map((nom, i) => [nom, prix[i]])
.filter(([, p]) => p < 100)
.toSorted((a, b) => a[1] - b[1]);
for (const [nom, p] of abordables) {
console.log(`${nom} — ${p} €`);
}
const total = abordables.reduce((somme, [, p]) => somme + p, 0);
console.log(`Total : ${total.toFixed(2)} €`);
La déstructuration fonctionne aussi dans les paramètres d'un
callback : ([, p]) récupère directement le prix de
chaque paire. Au chapitre suivant, on rangera plutôt chaque
composant dans un objet, bien plus lisible qu'une paire.
Un tableau range des valeurs numérotées. Mais pour décrire une réparation, des numéros ne disent rien : qu'est-ce que l'élément n° 2 ? Le client ? L'appareil ? Un objet range des valeurs sous des noms, comme les rubriques d'une fiche de prise en charge : client, appareil, panne.
const fiche = {
numero: 42,
client: "Mme Le Gall",
appareil: "PC portable",
panne: "écran noir au démarrage",
sousGarantie: false,
};
console.log(fiche);
Un objet s'écrit entre accolades. Chaque
propriété est une paire clé: valeur, et les
paires sont séparées par des virgules. La virgule après la dernière
propriété est facultative, mais pratique : on ajoute une ligne sans
toucher à la précédente.
const fiche = { client: "Mme Le Gall", appareil: "PC portable", "date de dépôt": "4 nov." };
console.log(fiche.client); // notation pointée : la plus courante
console.log(fiche["appareil"]); // notation crochets
console.log(fiche["date de dépôt"]); // obligatoire si la clé contient un espace
const rubrique = "appareil";
console.log(fiche[rubrique]); // ou si la clé est dans une variable
console.log(fiche.telephone); // absente : undefined
const fiche = { client: "Mme Le Gall", panne: "écran noir" };
fiche.diagnostic = "nappe d'écran débranchée"; // ajouter
fiche.panne = "écran noir intermittent"; // modifier
delete fiche.client; // supprimer (rare : on préfère null)
console.log(fiche);
console.log("diagnostic" in fiche); // la propriété existe-t-elle ?
console.log(Object.hasOwn(fiche, "client"));
Une propriété peut contenir n'importe quelle valeur : un tableau, ou un autre objet. C'est ainsi qu'on décrit des données réalistes :
const reparation = {
client: {
nom: "Yann Le Bihan",
telephone: "06 12 34 56 78",
adresse: { ville: "Morlaix", codePostal: "29600" },
},
pieces: ["SSD 1 To", "Pâte thermique"],
heures: 1.5,
};
console.log(reparation.client.adresse.ville);
console.log(reparation.pieces[0]);
console.log(reparation.pieces.length, "pièces");
Et le duo le plus courant de tout JavaScript : un tableau
d'objets. C'est la forme de presque toutes les données réelles
(catalogue, liste de clients, rendez-vous). console.table
l'affiche à merveille :
const catalogue = [
{ nom: "SSD 1 To", categorie: "stockage", prix: 89.9 },
{ nom: "RAM 16 Go", categorie: "mémoire", prix: 54 },
{ nom: "Ventirad", categorie: "refroidissement", prix: 32.5 },
];
console.table(catalogue);
const stockage = catalogue.filter(article => article.categorie === "stockage");
console.log(stockage.map(article => article.nom));
Une propriété peut aussi contenir une fonction. On
l'appelle alors une méthode : c'est une action que
l'objet sait faire. Tu en utilises depuis le début :
console.log est la méthode log de l'objet
console !
const atelier = {
nom: "L'Atelier Informatique du Finistère",
ville: "Morlaix",
// écriture abrégée d'une méthode (la plus courante)
presenter() {
return `${this.nom}, à ${this.ville}`;
},
};
console.log(atelier.presenter());
Dans une méthode, this désigne l'objet sur lequel
la méthode est appelée : celui qui se trouve devant le point.
Grâce à lui, une méthode accède aux autres propriétés de son objet.
const fiche = {
client: "Mme Le Gall",
heures: 2,
tauxHoraire: 45,
pieces: [89.9, 12],
total() {
const mainOeuvre = this.heures * this.tauxHoraire;
const fournitures = this.pieces.reduce((s, p) => s + p, 0);
return mainOeuvre + fournitures;
},
resumer() {
return `${this.client} : ${this.total()} €`; // une méthode peut en appeler une autre
},
};
console.log(fiche.resumer());
fiche.heures = 3; // on modifie l'objet…
console.log(fiche.resumer()); // … et les méthodes suivent
this dépend de la façon dont la fonction est
appelée, pas de l'endroit où elle est écrite. Si on détache une
méthode de son objet, elle perd son this. Et une
fonction fléchée n'a pas de this à elle :
ne l'utilise pas pour écrire une méthode.
const fiche = {
client: "Mme Le Gall",
nomClassique() { return this.client; },
nomFleche: () => this.client, // ✗ fléchée : this n'est pas la fiche
};
console.log(fiche.nomClassique()); // ✓ "Mme Le Gall"
console.log(fiche.nomFleche()); // ✗ undefined
const detachee = fiche.nomClassique;
console.log(detachee()); // ✗ undefined : appelée sans « fiche. » devant
Un getter (get) est une méthode qui se lit
comme une propriété, sans parenthèses : idéal pour une valeur calculée.
Un setter (set) est appelé quand on
affecte une valeur : il peut la vérifier ou la transformer.
const client = {
prenom: "Yann",
nom: "Le Bihan",
_telephone: "",
get nomComplet() {
return `${this.prenom} ${this.nom.toUpperCase()}`;
},
set telephone(numero) {
const chiffres = numero.replaceAll(" ", "").replaceAll(".", "");
if (chiffres.length !== 10) {
console.warn(`Numéro refusé : ${numero}`);
return;
}
this._telephone = chiffres;
},
get telephone() {
return this._telephone.replace(/(\d{2})(?=\d)/g, "$1 ");
},
};
console.log(client.nomComplet); // pas de parenthèses !
client.telephone = "06.12.34"; // refusé par le setter
client.telephone = "0612345678";
console.log(client.telephone);
/(\d{2})(?=\d)/g est une
expression régulière (regex), un mini-langage pour décrire des
formes de texte. Ici : « deux chiffres suivis d'un autre chiffre ». Les
regex mériteraient un cours à elles seules ; retiens qu'elles existent
pour chercher et remplacer selon un modèle. Le préfixe _ de
_telephone signale par convention une propriété « interne »
à ne pas toucher directement. On verra une vraie protection avec les
classes.
Propriétés abrégées : quand une variable porte déjà le nom de la clé, inutile de l'écrire deux fois. Clés calculées : entre crochets, une expression donne le nom de la clé.
const client = "Mme Le Gall";
const appareil = "PC portable";
const fiche = { client, appareil }; // équivaut à { client: client, appareil: appareil }
console.log(fiche);
const champ = "statut";
const maj = { [champ]: "prêt", [`date_${champ}`]: "4 nov." };
console.log(maj);
La déstructuration marche aussi avec les objets, cette fois par nom de propriété. On peut renommer, donner une valeur par défaut, et même déstructurer directement les paramètres d'une fonction :
const fiche = { client: "Yann", appareil: "smartphone", heures: 1 };
const { client, appareil } = fiche;
console.log(client, appareil);
const { heures: duree, urgence = false } = fiche; // renommage et valeur par défaut
console.log(duree, urgence);
// Dans les paramètres : l'ordre des informations n'a plus d'importance
function etiquette({ client, appareil, urgence = false }) {
return `${urgence ? "🔴 " : ""}${appareil} de ${client}`;
}
console.log(etiquette(fiche));
console.log(etiquette({ appareil: "serveur", client: "Mairie", urgence: true }));
Comme les tableaux, les objets sont manipulés par
référence : const copie = fiche ne copie
rien, ça donne un second nom au même objet. Pour une vraie copie, on
décompose l'objet dans un nouveau avec ... :
const modele = { tauxHoraire: 45, deplacement: 0, devis: 20 };
const tarifDomicile = { ...modele, deplacement: 15 }; // copie + modification
console.log(modele, tarifDomicile);
const options = { urgence: true };
const complet = { ...tarifDomicile, ...options }; // fusion
console.log(complet);
{ ...objet } ne copie que le premier niveau. Si une propriété
contient un objet ou un tableau, la copie partage la
même référence. Pour une copie complète, à tous les niveaux,
utilise structuredClone(objet).
const fiche = { client: "Yann", pieces: ["SSD"] };
const copieSuperficielle = { ...fiche };
const copieComplete = structuredClone(fiche);
copieSuperficielle.pieces.push("RAM");
console.log(fiche.pieces); // ✗ modifié aussi : tableau partagé
console.log(copieComplete.pieces); // ✓ indépendant
const tarifs = { diagnostic: 0, devis: 20, heure: 45, deplacement: 15 };
console.log(Object.keys(tarifs)); // les clés
console.log(Object.values(tarifs)); // les valeurs
console.log(Object.entries(tarifs)); // les paires [clé, valeur]
for (const [prestation, prix] of Object.entries(tarifs)) {
console.log(`${prestation.padEnd(12, ".")} ${prix} €`);
}
JSON (JavaScript Object Notation) est un format texte qui reprend la syntaxe des objets JavaScript. C'est la langue commune du web : un serveur qui envoie des données, un fichier de configuration, une sauvegarde dans le navigateur… tout passe par JSON.
const fiche = { numero: 42, client: "Mme Le Gall", pieces: ["SSD"], payee: false };
const texte = JSON.stringify(fiche); // objet → texte JSON
console.log(texte, "→", typeof texte);
console.log(JSON.stringify(fiche, null, 2)); // version indentée, lisible
const recu = '{"numero": 43, "client": "Yann", "pieces": []}';
const objet = JSON.parse(recu); // texte JSON → objet
console.log(objet.client, objet.numero + 1);
JSON.stringify ignore les fonctions et les valeurs
undefined : seules les données voyagent, jamais le
comportement. Un texte mal formé fait échouer JSON.parse :
console.log(JSON.stringify({ client: "Yann", total() { return 0; }, note: undefined }));
JSON.parse("{client: 'Yann'}"); // clés et guillemets simples : refusé
Si les chaînes sont des primitifs, comment "SSD".length ou
"ssd".toUpperCase() peuvent-ils fonctionner ? Astuce du
moteur : quand tu utilises une méthode sur un primitif, JavaScript
l'enveloppe un instant dans un objet temporaire
(String, Number, Boolean), appelle
la méthode, puis jette l'emballage. Tu profites des méthodes, le primitif
reste un primitif. N'écris jamais new String("SSD") toi-même :
cet emballage permanent ne sert à rien et complique les comparaisons.
ficheReparation avec un client, un tableau
lignes vide et trois méthodes :
ajouterLigne(libelle, montant) qui ajoute un objet
{ libelle, montant } au tableau, un getter
total qui additionne les montants, et
imprimer() qui affiche chaque ligne puis le total. Ajoute
trois lignes et imprime la fiche.
const ficheReparation = {
client: "M. Kerboriou",
lignes: [],
ajouterLigne(libelle, montant) {
this.lignes.push({ libelle, montant });
},
get total() {
return this.lignes.reduce((somme, ligne) => somme + ligne.montant, 0);
},
imprimer() {
console.log(`Fiche de ${this.client}`);
for (const { libelle, montant } of this.lignes) {
console.log(` ${libelle.padEnd(22, ".")} ${montant.toFixed(2)} €`);
}
console.log(` ${"TOTAL".padEnd(22, ".")} ${this.total.toFixed(2)} €`);
},
};
ficheReparation.ajouterLigne("SSD 1 To", 89.9);
ficheReparation.ajouterLigne("Main d'œuvre (1,5 h)", 67.5);
ficheReparation.ajouterLigne("Devis déduit", -20);
ficheReparation.imprimer();
Tableaux et objets couvrent 90 % des besoins. Pour les 10 % restants,
JavaScript propose deux collections spécialisées, plus adaptées à
certaines situations : Set et Map.
Un Set (« ensemble ») contient des valeurs uniques : ajouter une valeur déjà présente ne fait rien. Parfait pour répondre à « quelles marques ai-je déjà réparées ? » sans compter deux fois la même.
const marquesReparees = new Set();
marquesReparees.add("Asus");
marquesReparees.add("Lenovo");
marquesReparees.add("Asus"); // déjà présente : ignorée
marquesReparees.add("HP");
console.log(marquesReparees);
console.log(marquesReparees.size); // nombre d'éléments (pas length !)
console.log(marquesReparees.has("Lenovo")); // présente ?
marquesReparees.delete("HP");
for (const marque of marquesReparees) {
console.log(`- ${marque}`);
}
Son usage vedette : dédoublonner un tableau en une ligne.
const villesClients = ["Morlaix", "Brest", "Morlaix", "Lannion", "Brest", "Morlaix"];
const villesUniques = [...new Set(villesClients)];
console.log(villesUniques);
console.log(`${villesUniques.length} villes desservies`);
has() sur un Set est quasi instantané, même avec des
milliers d'éléments, alors que includes() sur un tableau doit
parcourir les éléments un par un. Pour des vérifications d'appartenance
répétées sur une grande liste, le Set est le bon outil.
Une Map associe des clés à des valeurs, comme un objet. Mais ses clés peuvent être de n'importe quel type (nombre, objet…), elle garde l'ordre d'insertion, connaît sa taille et propose des méthodes claires :
const stock = new Map();
stock.set("SSD 1 To", 8);
stock.set("RAM 16 Go", 3);
stock.set("Ventirad", 0);
console.log(stock);
console.log(stock.get("RAM 16 Go")); // lire
console.log(stock.has("Carte mère")); // la clé existe ?
console.log(stock.size);
stock.set("RAM 16 Go", stock.get("RAM 16 Go") - 1); // une barrette posée
stock.delete("Ventirad");
for (const [piece, quantite] of stock) {
console.log(`${piece} : ${quantite}`);
}
On passe facilement d'un objet ou d'un tableau à une Map, et inversement :
const tarifs = { diagnostic: 0, devis: 20, heure: 45 };
const carteTarifs = new Map(Object.entries(tarifs)); // objet → Map
console.log(carteTarifs);
console.log([...carteTarifs]); // Map → tableau de paires
console.log(Object.fromEntries(carteTarifs)); // Map → objet
carteTarifs.clear(); // tout vider
console.log(carteTarifs.size);
| Besoin | Choix |
|---|---|
| décrire une chose avec des rubriques fixes (une fiche client) | objet |
| données à envoyer en JSON | objet |
| dictionnaire qui grossit et rétrécit souvent (un stock, un cache) | Map |
| clés qui ne sont pas du texte (nombres, objets) | Map |
| besoin fréquent de la taille ou de l'ordre d'insertion | Map |
Chaque objet, tableau ou chaîne que tu crées occupe de la mémoire. Qui la libère ? En JavaScript, personne n'a besoin de le faire à la main : un mécanisme automatique, le ramasse-miettes (garbage collector), s'en charge.
Son principe : une valeur reste en mémoire tant qu'elle est atteignable, c'est-à-dire tant qu'on peut y accéder en partant d'une variable du programme, de proche en proche. Dès qu'une valeur n'est plus reliée à rien, elle devient une « miette » : le ramasse-miettes la supprime lors de son prochain passage.
let fiche = { client: "Yann", pieces: ["SSD"] }; // l'objet est atteignable via « fiche »
let archive = fiche; // deux références vers le même objet
fiche = null; // plus atteignable par « fiche »… mais toujours par « archive »
archive = null; // plus aucune référence : l'objet est bon pour la benne
Imagine le débarras de l'atelier : tant qu'une pièce figure sur une fiche en cours ou dans l'inventaire, on la garde. Une pièce que plus aucune fiche ne mentionne finit à la déchetterie lors du prochain grand ménage, sans que personne ait besoin d'y penser.
WeakMap et WeakSet sont des variantes « faibles »
de Map et Set. Leurs clés doivent être des objets, et
elles ne les retiennent pas : si l'objet n'est plus référencé ailleurs, le
ramasse-miettes peut le supprimer, et son entrée disparaît de la
collection.
// Mémoriser des informations sur des fiches, sans les empêcher d'être libérées
const tempsPasse = new WeakMap();
const dejaFacturees = new WeakSet();
let fiche = { client: "Yann" };
tempsPasse.set(fiche, 90); // 90 minutes passées sur cette fiche
dejaFacturees.add(fiche);
console.log(tempsPasse.get(fiche), "min");
console.log(dejaFacturees.has(fiche));
console.log(tempsPasse); // contenu invisible : il peut disparaître à tout moment
fiche = null; // la fiche et ses informations pourront être libérées ensemble
Conséquence : une collection faible n'a ni size, ni boucle
possible (son contenu peut changer à tout instant). Elle sert à attacher
des informations à des objets qui appartiennent à quelqu'un d'autre, sans
prolonger leur durée de vie. Un usage avancé : tu sauras qu'il existe le
jour où tu en auras besoin.
Set | Map | WeakSet | WeakMap | |
|---|---|---|---|---|
| Contient | valeurs uniques | paires clé → valeur | objets uniques | paires objet → valeur |
| Types de clés | tous | tous | objets seulement | objets seulement |
size et boucles | oui | oui | non | non |
| Retient ses objets | oui | oui | non | non |
map.get(panne) ?? 0 donne le compte
actuel, même la première fois.
const pannes = ["écran", "batterie", "lenteur", "écran", "virus", "lenteur", "écran", "batterie", "lenteur", "lenteur"];
// À toi d'écrire la suite…
const pannes = ["écran", "batterie", "lenteur", "écran", "virus", "lenteur", "écran", "batterie", "lenteur", "lenteur"];
const compteur = new Map();
for (const panne of pannes) {
compteur.set(panne, (compteur.get(panne) ?? 0) + 1);
}
const classement = [...compteur].toSorted((a, b) => b[1] - a[1]);
for (const [panne, nombre] of classement) {
console.log(`${panne.padEnd(10)} ${"█".repeat(nombre)} ${nombre}`);
}
Un objet littéral, c'est parfait pour une fiche. Mais l'atelier en remplit des dizaines par mois, toutes construites sur le même modèle. Comment fabriquer des objets en série ? Ce chapitre explique le mécanisme interne de JavaScript, celui sur lequel reposent les classes du chapitre suivant.
La solution la plus simple : une fonction qui fabrique et renvoie un objet.
function creerFiche(client, appareil) {
return {
client,
appareil,
statut: "reçu",
resumer() {
return `${this.appareil} de ${this.client} (${this.statut})`;
},
};
}
const fiche1 = creerFiche("Mme Le Gall", "PC portable");
const fiche2 = creerFiche("Yann", "smartphone");
console.log(fiche1.resumer());
console.log(fiche2.resumer());
console.log(fiche1.resumer === fiche2.resumer); // deux copies de la même méthode…
Ça marche, mais la dernière ligne révèle un gaspillage : chaque fiche
transporte sa propre copie de la méthode
resumer. Avec mille fiches, mille copies identiques. Les
prototypes règlent ce problème.
Une fonction constructeur est une fonction ordinaire,
nommée en PascalCase par convention, qu'on appelle avec le mot-clé
new :
function Fiche(client, appareil) {
this.client = client;
this.appareil = appareil;
this.statut = "reçu";
}
const fiche = new Fiche("Mme Le Gall", "PC portable");
console.log(fiche);
console.log(fiche instanceof Fiche); // fabriquée par Fiche ?
new effectue quatre opérations, dans l'ordre :
this désignant le nouvel objet ;new. Sans lui, Fiche("Yann", "PC")
s'exécute comme une fonction normale : rien n'est créé, elle renvoie
undefined, et this ne désigne pas une nouvelle
fiche. Les classes du chapitre suivant rendent cet oubli impossible : elles
refusent de s'exécuter sans new.
Chaque fonction possède une propriété prototype : un objet
partagé par tous les objets qu'elle construit. On y range
les méthodes, une seule fois :
function Fiche(client, appareil) {
this.client = client;
this.appareil = appareil;
this.statut = "reçu";
}
Fiche.prototype.resumer = function () {
return `${this.appareil} de ${this.client} (${this.statut})`;
};
Fiche.prototype.terminer = function () {
this.statut = "prêt";
};
const fiche1 = new Fiche("Mme Le Gall", "PC portable");
const fiche2 = new Fiche("Yann", "smartphone");
fiche2.terminer();
console.log(fiche1.resumer());
console.log(fiche2.resumer());
console.log(fiche1.resumer === fiche2.resumer); // une seule méthode, partagée
console.log(fiche1); // la méthode n'est pas DANS l'objet
Quand on lit une propriété, JavaScript la cherche d'abord dans l'objet
lui-même. S'il ne la trouve pas, il remonte vers le prototype de l'objet,
puis vers le prototype de ce prototype, et ainsi de suite jusqu'à
Object.prototype, la racine commune. Cette remontée s'appelle
la chaîne de prototypes. C'est ce qu'on appelle
l'héritage en JavaScript.
function Fiche(client) {
this.client = client;
}
Fiche.prototype.resumer = function () {
return `Fiche de ${this.client}`;
};
const fiche = new Fiche("Yann");
console.log(Object.hasOwn(fiche, "client")); // propriété propre : dans l'objet
console.log(Object.hasOwn(fiche, "resumer")); // non : héritée du prototype
console.log(Object.getPrototypeOf(fiche) === Fiche.prototype);
// toString n'est défini ni dans fiche, ni dans Fiche.prototype…
console.log(typeof fiche.toString); // … mais dans Object.prototype !
console.log(Object.getPrototypeOf(Fiche.prototype) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype)); // fin de la chaîne
map() ou push(), c'est
parce que tous les tableaux ont pour prototype
Array.prototype, où ces méthodes sont rangées. Même chose pour
les chaînes avec String.prototype. Dans la console de ton
navigateur, tape Array.prototype et déplie le résultat : toutes
les méthodes des tableaux sont là.
Object.create(proto) crée un nouvel objet dont le prototype
est l'objet de ton choix, sans passer par un constructeur. Son second
argument, facultatif, permet de définir des propriétés avec des options
fines :
const modeleIntervention = {
resumer() {
return `${this.type} : ${this.duree} h`;
},
};
const nettoyage = Object.create(modeleIntervention);
nettoyage.type = "Nettoyage complet";
nettoyage.duree = 1;
console.log(nettoyage.resumer());
const montage = Object.create(modeleIntervention, {
type: { value: "Montage PC", enumerable: true },
duree: { value: 3, enumerable: true, writable: false }, // en lecture seule
});
montage.duree = 1; // ignoré : propriété non modifiable
console.log(montage.resumer());
const dictionnaire = Object.create(null); // aucun prototype : même pas toString
console.log(typeof dictionnaire.toString);
Object.setPrototypeOf(objet, proto) change le prototype d'un
objet existant, et l'ancienne propriété __proto__
permet de le lire ou de le modifier. Tu les croiseras dans d'anciens
tutoriels :
const appareil = { allumer() { return `${this.nom} démarre…`; } };
const portable = { nom: "PC portable" };
Object.setPrototypeOf(portable, appareil);
console.log(portable.allumer());
console.log(portable.__proto__ === appareil); // ancienne syntaxe
__proto__ est une relique conservée
pour la compatibilité. Choisis le prototype dès la création
(Object.create, new, ou mieux : une classe), et
utilise Object.getPrototypeOf() pour le lire.
Appareil(type, marque, annee) et ajoute
à son prototype une méthode age() qui renvoie l'âge de
l'appareil (utilise new Date().getFullYear() pour l'année en
cours, on étudiera les dates dans la partie 5) et une méthode
conseil() qui renvoie « Réparation conseillée » sous 6 ans,
« Remplacement à étudier » au-delà. Crée deux appareils et affiche
leurs conseils.
function Appareil(type, marque, annee) {
this.type = type;
this.marque = marque;
this.annee = annee;
}
Appareil.prototype.age = function () {
return new Date().getFullYear() - this.annee;
};
Appareil.prototype.conseil = function () {
return this.age() < 6 ? "Réparation conseillée" : "Remplacement à étudier";
};
const appareils = [new Appareil("Portable", "Lenovo", 2022), new Appareil("Tour", "HP", 2014)];
for (const a of appareils) {
console.log(`${a.type} ${a.marque} (${a.age()} ans) : ${a.conseil()}`);
}
Une classe est un plan de fabrication.
Le plan d'un PC décrit ses composants et ce qu'il sait faire ; chaque PC
assemblé à partir de ce plan est une instance, avec son
propre numéro de série et ses propres réglages. Depuis 2015, JavaScript
propose le mot-clé class : une écriture claire et moderne
pour ce que le chapitre précédent faisait avec les constructeurs et les
prototypes.
class Fiche {
constructor(client, appareil) {
this.client = client;
this.appareil = appareil;
this.statut = "reçu";
}
resumer() {
return `${this.appareil} de ${this.client} (${this.statut})`;
}
terminer() {
this.statut = "prêt";
}
}
const fiche1 = new Fiche("Mme Le Gall", "PC portable");
const fiche2 = new Fiche("Yann", "smartphone");
fiche2.terminer();
console.log(fiche1.resumer());
console.log(fiche2.resumer());
console.log(fiche1);
constructor est appelé automatiquement par new : il initialise les propriétés de l'instance avec this ;Les propriétés qui ont une valeur de départ fixe peuvent être déclarées en tête de classe, comme des champs. On voit d'un coup d'œil ce que contient chaque instance :
class Fiche {
statut = "reçu"; // champ avec valeur par défaut
pieces = []; // chaque instance aura SON propre tableau
constructor(client, appareil) {
this.client = client;
this.appareil = appareil;
}
ajouterPiece(piece) {
this.pieces.push(piece);
return this; // renvoyer l'objet permet d'enchaîner les appels
}
}
const fiche = new Fiche("Yann", "PC portable");
fiche.ajouterPiece("SSD 1 To").ajouterPiece("Pâte thermique");
console.log(fiche);
Les getters et setters du chapitre 13 s'écrivent de la même façon dans une classe :
class LignePanier {
constructor(article, prixUnitaire, quantite = 1) {
this.article = article;
this.prixUnitaire = prixUnitaire;
this.quantite = quantite;
}
get total() {
return this.prixUnitaire * this.quantite;
}
set quantite(valeur) {
if (!Number.isInteger(valeur) || valeur < 1) {
throw new Error(`Quantité invalide : ${valeur}`);
}
this._quantite = valeur;
}
get quantite() {
return this._quantite;
}
}
const ligne = new LignePanier("RAM 16 Go", 54, 2);
console.log(`${ligne.article} × ${ligne.quantite} = ${ligne.total} €`);
ligne.quantite = 0; // le setter refuse et lève une erreur
throw new Error("…") lève une erreur : le
programme s'arrête, exactement comme avec les erreurs que tu provoques
depuis le début du cours. C'est la façon propre de refuser une valeur
incorrecte plutôt que de la laisser passer en silence. On verra dans la
partie 5 comment rattraper une erreur avec
try…catch.
Avec la convention du _, la propriété
_quantite reste accessible et modifiable de l'extérieur : ce
n'est qu'un panneau « prière de ne pas toucher ». N'importe qui peut
écrire ligne._quantite = -5 et contourner la vérification.
Le préfixe # rend un champ ou une méthode
réellement privé : seul le code écrit
à l'intérieur de la classe peut y accéder. C'est
l'encapsulation :
class CarteFidelite {
#points = 0; // champ privé
constructor(client) {
this.client = client;
}
crediter(montantDepense) {
this.#points += Math.floor(montantDepense / 10);
this.#verifierPalier();
}
get points() { // lecture autorisée, modification impossible
return this.#points;
}
#verifierPalier() { // méthode privée
if (this.#points >= 30) {
console.log(`🎁 ${this.client} a droit à un nettoyage gratuit !`);
}
}
}
const carte = new CarteFidelite("Mme Le Gall");
carte.crediter(129.9);
carte.crediter(189);
console.log(carte.points, "points");
carte.points = 1000; // ignoré : pas de setter
console.log(carte.points, "points");
console.log(carte); // le champ privé n'apparaît même pas
Et toute tentative d'accès depuis l'extérieur est refusée avant même l'exécution :
class CarteFidelite {
#points = 0;
}
const carte = new CarteFidelite();
console.log(carte.#points);
Un membre static appartient à la classe
elle-même, pas à ses instances. On l'utilise pour une donnée
commune à toutes (un compteur, une constante) ou une fonction utilitaire
liée au thème de la classe :
class Ticket {
static #dernierNumero = 41;
static PREFIXE = "R-";
constructor(client) {
Ticket.#dernierNumero++;
this.numero = `${Ticket.PREFIXE}${String(Ticket.#dernierNumero).padStart(4, "0")}`;
this.client = client;
}
static combienEmis() {
return Ticket.#dernierNumero - 41;
}
}
const t1 = new Ticket("Yann");
const t2 = new Ticket("Mme Le Gall");
console.log(t1.numero, t2.numero);
console.log(Ticket.combienEmis(), "tickets émis aujourd'hui");
console.log(t1.combienEmis); // undefined : pas accessible depuis une instance
Math.round(), Number.isNaN(),
Array.isArray(), Object.keys() sont des méthodes
statiques. On les appelle sur Math, Number,
Array ou Object, jamais sur une valeur
particulière.
Les interventions de l'atelier ont toutes un client et une durée, mais
chaque type se facture à sa manière. Plutôt que de tout réécrire, on crée
une classe parente commune, et des classes
enfants qui l'étendent avec
extends : elles héritent de tout, et ajoutent ou redéfinissent
ce qui leur est propre.
class Intervention {
static TAUX_HORAIRE = 45;
constructor(client, heures) {
this.client = client;
this.heures = heures;
}
prix() {
return this.heures * Intervention.TAUX_HORAIRE;
}
resumer() {
return `${this.constructor.name} pour ${this.client} : ${this.prix()} €`;
}
}
class Reparation extends Intervention {
constructor(client, heures, pieces) {
super(client, heures); // appelle le constructeur parent : obligatoire, et en premier
this.pieces = pieces;
}
prix() {
const fournitures = this.pieces.reduce((s, p) => s + p, 0);
return super.prix() + fournitures; // réutilise le calcul du parent
}
}
class Diagnostic extends Intervention {
prix() {
return 0; // le diagnostic est gratuit
}
}
const reparation = new Reparation("Mme Le Gall", 1.5, [89.9]);
const diagnostic = new Diagnostic("Yann", 0.5);
console.log(reparation.resumer());
console.log(diagnostic.resumer());
console.log(reparation instanceof Reparation, reparation instanceof Intervention);
Quelques règles à retenir :
constructor doit appeler super(…) avant d'utiliser this ;Diagnostic) ;super.methode() appelle la version du parent depuis une méthode redéfinie ;resumer(), écrite une seule fois dans le parent, appelle le prix() de chaque enfant.Ce dernier point a un nom : le polymorphisme (« plusieurs formes »). Des objets de classes différentes répondent à la même méthode, chacun à sa manière. Le code qui les utilise n'a pas besoin de savoir à quel type précis il a affaire :
class Intervention {
constructor(client, heures) {
this.client = client;
this.heures = heures;
}
prix() { return this.heures * 45; }
}
class Diagnostic extends Intervention {
prix() { return 0; }
}
class Montage extends Intervention {
prix() { return super.prix() + 30; } // + forfait tests et installation du système
}
class Domicile extends Intervention {
constructor(client, heures, km) {
super(client, heures);
this.km = km;
}
prix() { return super.prix() + this.km * 0.5; }
}
const journee = [
new Diagnostic("Yann", 0.5),
new Montage("Erwan", 3),
new Domicile("Mme Le Gall", 1, 24),
];
let chiffreDuJour = 0;
for (const intervention of journee) {
const prix = intervention.prix(); // chacun calcule à sa façon
chiffreDuJour += prix;
console.log(`${intervention.constructor.name.padEnd(10)} ${intervention.client.padEnd(12)} ${prix} €`);
}
console.log(`Chiffre du jour : ${chiffreDuJour} €`);
Sous le capot, une classe est une fonction constructeur avec son
prototype : typeof Fiche répond "function". Mais
elle apporte des garde-fous bienvenus :
class Fiche {
constructor(client) { this.client = client; }
}
console.log(typeof Fiche);
console.log(Object.getOwnPropertyNames(Fiche.prototype)); // les méthodes vivent dans le prototype
const fiche = Fiche("Yann"); // oubli de new : refusé net
| Fonction constructeur | Classe | |
|---|---|---|
| Méthodes | ajoutées à part, sur le prototype | écrites dans le corps de la classe |
Oubli de new | bug silencieux | erreur immédiate |
| Héritage | Object.create et bricolage | extends et super |
| Champs privés | impossible (convention _) | #champ |
| Utilisable avant sa déclaration | oui (hissée) | non |
| Verdict | à savoir lire | à utiliser |
Client avec un nom, un champ privé
#depenses (à 0), une méthode enregistrer(montant),
un getter fidele (vrai au-delà de 300 € dépensés) et une
méthode remise(montant) qui renvoie 10 % du montant pour un
client fidèle, 0 sinon. Crée ensuite une classe
ClientPro qui étend Client, reçoit en plus un
SIRET, et accorde toujours 15 %. Teste les deux sur une facture de 200 €.
class Client {
#depenses = 0;
constructor(nom) {
this.nom = nom;
}
enregistrer(montant) {
this.#depenses += montant;
}
get fidele() {
return this.#depenses > 300;
}
remise(montant) {
return this.fidele ? montant * 0.1 : 0;
}
}
class ClientPro extends Client {
constructor(nom, siret) {
super(nom);
this.siret = siret;
}
remise(montant) {
return montant * 0.15;
}
}
const particulier = new Client("Yann Le Bihan");
particulier.enregistrer(129.9);
particulier.enregistrer(189);
const pro = new ClientPro("Cabinet Kerboriou", "123 456 789 00012");
for (const client of [particulier, pro]) {
const remise = client.remise(200);
console.log(`${client.nom} : remise de ${remise.toFixed(2)} € → ${(200 - remise).toFixed(2)} €`);
}
La boucle finale ne se soucie pas du type de client : c'est le
polymorphisme qui choisit la bonne méthode remise.
Note que ClientPro hérite aussi de
enregistrer et fidele, même s'il ne s'en
sert pas pour sa remise.
Tu sais désormais organiser des données de toutes les formes ! 🎉 Tu sais :
map, filter, reduce, sans te faire piéger par sort ;this, ses getters et setters ;structuredClone) en gardant en tête la notion de référence ;Map et Set, et comprendre le rôle du ramasse-miettes ;// Tout ce que tu sais faire, en quelques lignes
class Article {
constructor(nom, categorie, prix) {
Object.assign(this, { nom, categorie, prix });
}
}
const catalogue = [
new Article("SSD 1 To", "stockage", 89.9),
new Article("Disque 4 To", "stockage", 99),
new Article("RAM 16 Go", "mémoire", 54),
new Article("RAM 32 Go", "mémoire", 98),
];
const parCategorie = new Map();
for (const { categorie, prix } of catalogue) {
parCategorie.set(categorie, (parCategorie.get(categorie) ?? 0) + prix);
}
console.table(catalogue);
console.log(JSON.stringify(Object.fromEntries(parCategorie)));
Dans la partie 4, tout ça prend vie à l'écran : JavaScript va enfin parler au navigateur (le BOM), lire et modifier la page elle-même (le DOM), et réagir aux clics, aux touches et aux formulaires (les événements). Les objets et les méthodes que tu maîtrises maintenant seront partout.
filter et map pour répondre à des questions, puis
une classe si des comportements apparaissent. Modéliser des données,
c'est 80 % du travail d'un développeur.
Jusqu'ici, ton code calculait dans son coin. Mais JavaScript tourne dans un navigateur, et celui-ci lui ouvre un accès à tout son environnement : la fenêtre, l'écran, l'adresse de la page, l'historique, les minuteurs, le stockage… Cet ensemble d'objets s'appelle le BOM (Browser Object Model, « modèle d'objets du navigateur »).
Au sommet du BOM trône window : il représente l'onglet du
navigateur. C'est l'objet global : tout ce qui est
disponible partout (console, alert,
Math, setTimeout…) est en réalité une propriété
de window. On peut donc écrire alert() ou
window.alert() : c'est la même chose.
| Objet | Représente | Exemple d'usage |
|---|---|---|
window | l'onglet, l'objet global | taille de la fenêtre, défilement |
screen | l'écran physique | résolution, orientation |
navigator | le navigateur lui-même | langue, connexion, presse-papiers |
location | l'adresse de la page | lire l'URL, rediriger |
history | l'historique de l'onglet | page précédente |
localStorage, document.cookie | le stockage | mémoriser une préférence |
document | la page HTML elle-même | c'est le DOM, chapitres 18 et 19 |
console.log(window.innerWidth, "×", window.innerHeight, "px : zone d'affichage");
console.log(window.outerWidth, "×", window.outerHeight, "px : fenêtre complète");
console.log("Défilement vertical :", Math.round(window.scrollY), "px");
console.log("Densité de pixels :", window.devicePixelRatio);
console.log(window.Math === Math); // Math est une propriété de window
Redimensionne ta fenêtre (ou fais défiler la page) puis relance l'exemple :
les valeurs changent. Encore plus utile, matchMedia() teste
une media query, exactement comme celles du cours CSS :
const petitEcran = window.matchMedia("(max-width: 600px)").matches;
const themeSombre = window.matchMedia("(prefers-color-scheme: dark)").matches;
const animationsReduites = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
console.log("Petit écran :", petitEcran);
console.log("Thème sombre demandé :", themeSombre);
console.log("Animations réduites demandées :", animationsReduites);
var (et les fonctions
déclarées hors de tout bloc) deviennent des propriétés de
window. Un var name = "Yann" écrase ainsi
window.name, une propriété du navigateur. Encore une bonne
raison de préférer let et const, qui ne touchent
pas à window.
console.log("Écran :", screen.width, "×", screen.height);
console.log("Hors barre des tâches :", screen.availWidth, "×", screen.availHeight);
console.log("Orientation :", screen.orientation?.type ?? "inconnue");
console.log("Langue préférée :", navigator.language);
console.log("Toutes les langues :", navigator.languages);
console.log("En ligne :", navigator.onLine);
console.log("Cœurs du processeur :", navigator.hardwareConcurrency);
navigator.userAgent renvoie une longue chaîne censée
identifier le navigateur, mais elle est truffée de mentions trompeuses
(Chrome se présente aussi comme « Mozilla » et « Safari »…). Ne t'en sers
jamais pour décider quoi afficher : teste plutôt directement si la
fonctionnalité existe (if ("clipboard" in navigator)).
navigator donne aussi accès à des fonctions avancées, toujours
soumises à l'accord du visiteur ou à une action de sa part : la
géolocalisation, les notifications, ou le presse-papiers. Clique sur
Exécuter, puis colle (Ctrl+V) où tu veux :
navigator.clipboard.writeText("contact@ai29.tech")
.then(() => console.log("Adresse copiée dans le presse-papiers !"))
.catch(() => console.warn("Copie refusée par le navigateur."));
La syntaxe .then() / .catch() sert à attendre une
opération qui prend du temps : c'est une promesse, au programme de
la partie 5.
location découpe l'URL de la page en morceaux faciles à lire :
console.log("Adresse complète :", location.href);
console.log("Protocole :", location.protocol);
console.log("Domaine :", location.hostname);
console.log("Chemin :", location.pathname);
console.log("Ancre :", location.hash || "(aucune)");
Pour analyser n'importe quelle adresse, y compris ses paramètres
(la partie après le ?), les objets URL et
URLSearchParams font le travail proprement :
const adresse = new URL("https://ai29.tech/contact?service=devis&ville=Morlaix#formulaire");
console.log(adresse.hostname, adresse.pathname, adresse.hash);
console.log(adresse.searchParams.get("service"));
console.log(adresse.searchParams.get("ville"));
console.log(adresse.searchParams.has("urgence"));
adresse.searchParams.set("urgence", "oui");
console.log(adresse.href);
Enfin, location permet de changer de page.
Ces lignes ne sont pas exécutables ici, elles te feraient quitter le cours :
location.href = "https://ai29.tech"; // aller à une autre adresse
location.assign("https://ai29.tech"); // idem
location.replace("https://ai29.tech"); // idem, sans laisser de trace dans l'historique
location.reload(); // recharger la page
console.log(history.length); // nombre de pages dans l'historique de l'onglet
history.back(); // comme le bouton « Précédent »
history.forward(); // comme le bouton « Suivant »
history.go(-2); // reculer de deux pages
Pour des raisons de confidentialité, on ne peut pas lire les adresses
visitées : seulement leur nombre, et s'y déplacer. Les applications
modernes utilisent aussi history.pushState() pour changer
l'adresse affichée sans recharger la page.
alert(), confirm() et prompt(), vus
au chapitre 03, font eux aussi partie du BOM :
ce sont des méthodes de window. Rappel : pratiques pour tester,
à éviter sur un vrai site au profit de messages intégrés à la page.
Tu connais setTimeout() depuis le chapitre 11. Il a un frère,
setInterval(), qui répète une fonction à intervalle
régulier. Chacun renvoie un identifiant qui permet de l'annuler
avec clearTimeout() ou clearInterval().
let secondes = 5;
console.log(`Sauvegarde automatique dans ${secondes} s…`);
const minuteur = setInterval(() => {
secondes--;
if (secondes > 0) {
console.log(`${secondes}…`);
} else {
clearInterval(minuteur); // indispensable, sinon ça ne s'arrête jamais
console.log("✅ Sauvegarde effectuée.");
}
}, 1000);
const rappel = setTimeout(() => {
console.log("Ce message ne s'affichera jamais.");
}, 2000);
console.log("Rendez-vous annulé par le client.");
clearTimeout(rappel); // on annule le minuteur avant son déclenchement
requestAnimationFrame(), synchronisé avec l'affichage de
l'écran.
Un cookie est un petit morceau de texte que le navigateur
garde pour un site, et qu'il renvoie au serveur à chaque
requête. C'est leur raison d'être : maintenir une session de connexion,
par exemple. On les lit et on les écrit avec
document.cookie, dont l'interface est… particulière :
// Créer (ou modifier) un cookie : une affectation = UN cookie, avec ses options
document.cookie = "theme=sombre; max-age=3600; path=/; SameSite=Lax";
document.cookie = "ville=Morlaix; max-age=3600; path=/; SameSite=Lax";
// Lire : on obtient TOUS les cookies dans une seule chaîne
console.log(document.cookie || "(aucun cookie : ouvre la page depuis un vrai site web)");
// Transformer cette chaîne en objet, plus pratique
const cookies = Object.fromEntries(
document.cookie.split("; ").filter(Boolean).map(paire => paire.split("="))
);
console.log(cookies);
// Supprimer : on redonne le cookie avec une durée de vie nulle
document.cookie = "theme=; max-age=0; path=/";
document.cookie = "ville=; max-age=0; path=/";
localStorage est un petit entrepôt clé → valeur
(quelques Mo) propre à chaque site, qui survit à la fermeture du
navigateur. Son jumeau sessionStorage fonctionne
pareil, mais s'efface quand on ferme l'onglet. Rien n'est envoyé au
serveur.
const CLE = "ai29-cours-js-lancements";
const lancements = Number(localStorage.getItem(CLE) ?? 0) + 1;
localStorage.setItem(CLE, lancements);
console.log(`Tu as lancé cet exemple ${lancements} fois.`);
console.log("Recharge la page : le compteur continue !");
Le stockage ne garde que du texte. Pour un objet ou un tableau, on passe par JSON (chapitre 13) :
const preferences = { theme: "sombre", taillePolice: 18, ville: "Morlaix" };
localStorage.setItem("ai29-cours-js-preferences", JSON.stringify(preferences));
const relues = JSON.parse(localStorage.getItem("ai29-cours-js-preferences"));
console.log(relues.theme, relues.taillePolice);
localStorage.removeItem("ai29-cours-js-preferences"); // supprimer une clé
console.log(localStorage.getItem("ai29-cours-js-preferences")); // null : absente
| Cookie | localStorage | sessionStorage | |
|---|---|---|---|
| Durée de vie | choisie (max-age) | illimitée | jusqu'à la fermeture de l'onglet |
| Envoyé au serveur | oui, à chaque requête | non | non |
| Capacité | ≈ 4 Ko | ≈ 5 Mo | ≈ 5 Mo |
| Usage typique | session de connexion | préférences (thème, langue) | brouillon de formulaire |
localStorage le nombre de
pauses prises et affiche-le à la fin.
const CLE_PAUSES = "ai29-cours-js-pauses";
let minutes = 3;
console.log(`☕ Pause café : ${minutes} min`);
const pause = setInterval(() => {
minutes--;
if (minutes > 0) {
console.log(`Encore ${minutes} min`);
return;
}
clearInterval(pause);
const total = Number(localStorage.getItem(CLE_PAUSES) ?? 0) + 1;
localStorage.setItem(CLE_PAUSES, total);
console.log(`🔧 Retour à l'établi ! (pause n° ${total})`);
}, 1000);
Le return joue ici le rôle du retour anticipé : tant
qu'il reste du temps, on s'arrête là ; le code de fin ne
s'exécute qu'au dernier tour.
Quand le navigateur charge une page, il ne garde pas le HTML sous forme de texte : il le transforme en un arbre d'objets, où chaque balise devient un objet JavaScript qu'on peut lire et modifier. Cet arbre s'appelle le DOM (Document Object Model). C'est lui qui permet à JavaScript de changer un texte, d'afficher un message ou d'ajouter une ligne à un tableau, sans recharger la page.
Pour ce chapitre et le suivant, les exemples ne s'exécutent plus dans le
cours lui-même, mais dans une mini-page isolée, affichée
dans le cadre Rendu de chaque exemple. Dans ce code,
document désigne cette mini-page. Voici la page de l'atelier
qui va nous servir :
Et voici l'arbre que le navigateur en tire :
document
└── html
├── head
└── body
├── header
│ ├── h1#titre
│ └── p.discret
├── ul#services
│ ├── li.service
│ ├── li.service.populaire
│ └── li.service
├── p
│ └── a
├── label
└── input#ville
Le vocabulaire est celui d'un arbre généalogique : ul est le
parent des trois li, qui sont ses
enfants et sont frères entre eux. Chaque
élément de l'arbre est un nœud (node).
const titre = document.getElementById("titre"); // par id : un seul élément
console.log(titre);
const services = document.getElementsByClassName("service"); // par classe : une collection
console.log(services);
const liens = document.getElementsByTagName("a"); // par balise
console.log(liens.length, "lien(s)");
Les deux dernières renvoient une HTMLCollection, une liste qui
ressemble à un tableau (avec length et des positions) sans en
être un : pas de map ni de forEach.
La méthode moderne accepte n'importe quel sélecteur CSS,
exactement ceux du cours CSS. querySelector() renvoie le
premier élément qui correspond (ou null),
querySelectorAll() les renvoie tous :
console.log(document.querySelector("#titre")); // par id
console.log(document.querySelector(".service")); // le PREMIER .service
console.log(document.querySelector(".service.populaire")); // deux classes à la fois
console.log(document.querySelector("header p")); // descendant
console.log(document.querySelector('a[href^="mailto:"]')); // par attribut
console.log(document.querySelector("li:last-child")); // pseudo-classe
console.log(document.querySelector(".introuvable")); // aucun : null
const tous = document.querySelectorAll(".service");
console.log(tous);
tous.forEach(li => console.log("-", li.textContent));
querySelector et querySelectorAll
partout : une seule syntaxe à retenir, celle que tu connais déjà grâce au
CSS. On peut aussi les appeler sur un élément, pour ne chercher qu'à
l'intérieur : liste.querySelectorAll("li").
querySelector renvoie
null, et la ligne suivante plante avec
« Cannot read properties of null ». C'est l'erreur DOM n° 1 :
faute de frappe dans le sélecteur, ou script exécuté avant que l'élément
existe (d'où defer, chapitre 02).
const titre = document.querySelector("#tirte"); // faute de frappe…
console.log(titre);
console.log(titre.textContent); // … et c'est le plantage
À partir d'un élément, on peut se déplacer vers ses voisins de l'arbre :
const devis = document.querySelector(".populaire");
console.log("Parent :", devis.parentElement);
console.log("Frère précédent :", devis.previousElementSibling.textContent);
console.log("Frère suivant :", devis.nextElementSibling.textContent);
const liste = document.querySelector("#services");
console.log("Enfants :", liste.children);
console.log("Premier :", liste.firstElementChild.textContent);
console.log("Dernier :", liste.lastElementChild.textContent);
// closest() remonte les ancêtres jusqu'au premier qui correspond au sélecteur
console.log("Ancêtre ul :", devis.closest("ul"));
console.log("Ancêtre header ?", devis.closest("header"));
childNodes,
firstChild, nextSibling…) comptent
tous les nœuds, y compris les retours à la ligne et espaces entre
les balises, qui deviennent des nœuds texte invisibles :
const liste = document.querySelector("#services");
console.log(liste.children.length, "enfants éléments");
console.log(liste.childNodes.length, "nœuds au total !");
console.log(liste.firstChild); // un nœud texte (le retour à la ligne)
console.log(liste.firstElementChild); // le premier li, ce qu'on voulait
const titre = document.querySelector("h1");
console.log(titre.textContent); // le texte seul
console.log(titre.tagName, titre.id);
const paragraphe = document.querySelector("p:not(.discret)");
console.log(paragraphe.textContent); // texte, balises retirées
console.log(paragraphe.innerHTML); // le HTML intérieur, balises comprises
const lien = document.querySelector("a");
console.log(lien.getAttribute("title")); // un attribut quelconque
console.log(lien.href); // la plupart existent aussi en propriété
const devis = document.querySelector(".populaire");
console.log(devis.dataset.prix); // attribut data-prix → dataset.prix
console.log(devis.classList.contains("populaire"));
console.log(devis.matches("li.service")); // correspond au sélecteur ?
console.log(document.querySelector("#ville").value); // contenu d'un champ
data-* sont faits pour accrocher des données à
un élément HTML (un prix, un identifiant, un statut). En JavaScript, ils
se retrouvent dans dataset, avec un nom converti en
camelCase : data-date-depot devient
dataset.dateDepot. Leur valeur est toujours du
texte : pense à Number() pour calculer.
Une NodeList a forEach, mais ni
map, ni filter, ni reduce. La
décomposition [...liste] (ou Array.from()) la
convertit en vrai tableau, et toute la partie 3 redevient disponible :
const services = [...document.querySelectorAll(".service")];
const noms = services.map(li => li.textContent);
const prix = services.map(li => Number(li.dataset.prix));
const payants = services.filter(li => Number(li.dataset.prix) > 0);
console.log(noms);
console.log(prix);
console.log(payants.length, "services payants");
populaire, et enfin le texte du sous-titre en majuscules.
// À toi d'écrire le code…
for (const li of document.querySelectorAll(".service")) {
const prix = Number(li.dataset.prix);
console.log(`${li.textContent} : ${prix === 0 ? "gratuit" : `${prix} €`}`);
}
console.log("Populaire :", document.querySelector(".populaire").textContent);
console.log(document.querySelector("header .discret").textContent.toUpperCase());
Un gabarit peut en contenir un autre :
`${prix} €` est un petit gabarit placé dans la
branche « sinon » du ternaire.
Trouver un élément n'est qu'une étape : le but est d'agir dessus. Chaque modification du DOM est immédiatement visible à l'écran. Observe le cadre Rendu de chaque exemple : la page de l'atelier d'origine y est transformée par le code. Rétablir et Exécuter repartent toujours de la page intacte.
const titre = document.querySelector("#titre");
titre.textContent = "AI29 — votre informaticien de proximité";
const sousTitre = document.querySelector(".discret");
sousTitre.innerHTML = "Diagnostic <strong>gratuit</strong>, devis 20 € déductibles";
const devis = document.querySelector(".populaire");
devis.textContent += " ⭐"; // ajouter à la fin du texte existant
textContent traite tout comme du texte brut ;
innerHTML interprète les balises. La différence devient
cruciale dès que le texte vient d'un visiteur :
const saisieVisiteur = "<em>Mon PC</em> fait un bruit bizarre";
document.querySelector("h1").textContent = saisieVisiteur; // affiché tel quel ✓
document.querySelector(".discret").innerHTML = saisieVisiteur; // balises interprétées
<img src="x" onerror="…code pirate…"> dans un formulaire
et que tu l'insères avec innerHTML, son code s'exécute dans la
page de tous ceux qui l'affichent. C'est une faille appelée
XSS (cross-site scripting), l'une des plus
répandues du web. Règle simple : textContent pour tout ce qui
vient de l'extérieur, innerHTML seulement pour du HTML que tu
as écrit toi-même.
const lien = document.querySelector("a");
lien.setAttribute("title", "Réponse sous 24 h");
lien.setAttribute("href", "mailto:devis@ai29.tech");
lien.textContent = "devis@ai29.tech";
console.log(lien.getAttribute("href"), lien.hasAttribute("target"));
const champ = document.querySelector("#ville");
champ.value = "Saint-Pol-de-Léon"; // propriété directe : plus simple
champ.placeholder = "Votre commune";
champ.disabled = true; // attribut booléen : true ou false
const devis = document.querySelector(".populaire");
devis.dataset.prix = "25"; // écrit l'attribut data-prix
devis.removeAttribute("class"); // retire complètement l'attribut
console.log(devis.outerHTML);
Plutôt que de modifier les styles un par un, la bonne pratique consiste à
préparer des classes en CSS, puis à les ajouter ou retirer
en JavaScript. C'est le rôle de classList :
const services = document.querySelectorAll(".service");
services[0].classList.add("selection"); // ajouter
services[1].classList.remove("populaire"); // retirer
services[2].classList.add("prix", "selection"); // plusieurs d'un coup
const titre = document.querySelector("h1");
titre.classList.toggle("cache"); // ajoute si absente, retire si présente
titre.classList.toggle("cache"); // … donc revient à l'état initial
services[0].classList.replace("selection", "prix"); // remplacer
console.log(services[2].classList);
console.log(services[2].classList.contains("selection"));
toggle() accepte un second argument, un booléen qui
force le résultat : bouton.classList.toggle("actif",
estConnecte) ajoute la classe si estConnecte est vrai,
la retire sinon. Une ligne au lieu d'un if /
else.
La propriété style donne accès aux styles en ligne de
l'élément. Les noms de propriétés CSS à tiret s'écrivent en camelCase :
background-color devient backgroundColor.
const titre = document.querySelector("h1");
titre.style.color = "#1f2937";
titre.style.backgroundColor = "#ffedd5";
titre.style.padding = "0.4rem 0.6rem";
titre.style.borderRadius = "6px";
// Lire le style final, calculé par le navigateur (toutes règles CSS confondues)
const calcule = getComputedStyle(titre);
console.log(calcule.fontSize, calcule.fontWeight);
console.log(titre.style.fontSize || "(pas de style en ligne pour font-size)");
Encore plus puissant : modifier une variable CSS (cours CSS, chapitre 16) change d'un coup tous les éléments qui l'utilisent. C'est ainsi qu'on bascule un site entier en thème sombre :
const page = document.documentElement; // l'élément html
page.style.setProperty("--accent", "#c2410c");
// Les éléments qui utilisent la variable suivent automatiquement
document.querySelector("h1").style.borderLeft = "6px solid var(--accent)";
document.querySelector("h1").style.paddingLeft = "0.5rem";
setTimeout(() => {
page.style.setProperty("--accent", "#1f2937"); // une ligne, tout change
console.log("Accent passé en charbon !");
}, 1500);
style aux valeurs vraiment dynamiques (une position
qui suit la souris, une barre de progression à 37 %). Pour tout le reste
(masquer, mettre en évidence, changer de thème), ajoute ou retire une
classe : l'apparence reste dans le CSS, le comportement dans le
JavaScript.
Créer un élément se fait en trois temps : on le
fabrique avec document.createElement(), on le
remplit, puis on l'insère dans la page.
Tant qu'il n'est pas inséré, il existe en mémoire mais reste invisible.
const liste = document.querySelector("#services");
const nouveau = document.createElement("li"); // 1. fabriquer
nouveau.textContent = "Nettoyage complet"; // 2. remplir
nouveau.className = "service";
nouveau.dataset.prix = "35";
liste.append(nouveau); // 3. insérer à la fin de la liste
const urgence = document.createElement("li");
urgence.textContent = "Intervention urgente";
liste.prepend(urgence); // au début de la liste
const note = document.createElement("p");
note.textContent = "Tarifs valables jusqu'au 31 décembre.";
note.className = "discret";
liste.after(note); // juste après la liste (frère)
console.log(liste.children.length, "services");
| Méthode | Position |
|---|---|
parent.append(el) | dernier enfant du parent |
parent.prepend(el) | premier enfant du parent |
reference.before(el) | juste avant la référence (même parent) |
reference.after(el) | juste après la référence (même parent) |
parent.appendChild(el) | comme append : ancienne méthode, un seul élément à la fois |
Pour insérer un petit morceau de HTML écrit par toi,
insertAdjacentHTML(position, html) est une alternative
pratique. Ses quatre positions ont des noms explicites :
const liste = document.querySelector("#services");
liste.insertAdjacentHTML("beforebegin", "<h2>Nos tarifs</h2>"); // avant la liste
liste.insertAdjacentHTML("afterbegin", "<li>Conseil gratuit</li>"); // premier enfant
liste.insertAdjacentHTML("beforeend", "<li>Montage sur mesure</li>"); // dernier enfant
liste.insertAdjacentHTML("afterend", '<p class="discret">Prix nets, TVA non applicable.</p>');
Voici le geste le plus courant du développement front-end : partir d'un tableau d'objets (venu d'un serveur, d'un fichier JSON…) et générer le HTML correspondant. Nouvelle page de démonstration, presque vide :
const interventions = [
{ heure: "9 h", client: "Mme Le Gall", type: "Diagnostic PC portable" },
{ heure: "11 h", client: "M. Kerboriou", type: "Remplacement SSD" },
{ heure: "14 h", client: "Yann", type: "Montage PC gamer" },
{ heure: "16 h 30", client: "Mairie de Plourin", type: "Maintenance réseau" },
];
const liste = document.querySelector("#planning");
const elements = interventions.map(({ heure, client, type }) => {
const li = document.createElement("li");
const spanHeure = document.createElement("span");
spanHeure.className = "heure";
spanHeure.textContent = heure;
li.append(spanHeure, `${type} — ${client}`); // append accepte aussi du texte
return li;
});
liste.append(...elements); // une seule insertion pour tous les éléments
document.querySelector("#resume").textContent = `${interventions.length} interventions prévues.`;
append(...elements)) est plus efficace que de les insérer un
par un dans une boucle. Et en construisant avec
createElement et textContent, les noms de clients
ne risquent aucune injection de HTML.
const services = document.querySelectorAll(".service");
services[0].remove(); // supprimer un élément
const nouveauTitre = document.createElement("h2");
nouveauTitre.textContent = "Nos services";
document.querySelector("h1").replaceWith(nouveauTitre); // remplacer
const liste = document.querySelector("#services");
console.log(liste.children.length, "services restants");
document.querySelector("header .discret").replaceChildren(); // vider (garde l'élément)
console.log(document.querySelector("header .discret").outerHTML);
Dans du code ancien, tu verras parent.removeChild(enfant) et
parent.replaceChild(nouveau, ancien) : mêmes effets, en plus
verbeux.
cloneNode(true) copie un élément avec tout son contenu. Et la
balise HTML <template> est faite pour ça : son contenu
n'est jamais affiché, il sert de moule qu'on clone autant
de fois que nécessaire. On écrit la structure en HTML, et JavaScript ne
fait que remplir les trous :
const formules = [
{ nom: "Diagnostic", prix: "Gratuit", description: "On identifie la panne, sans engagement." },
{ nom: "Devis détaillé", prix: "20 €", description: "Déduits de la facture si vous validez." },
{ nom: "Intervention", prix: "Selon devis", description: "À l'atelier ou à domicile." },
];
const modele = document.querySelector("#modele-formule");
const zone = document.querySelector("#formules");
for (const formule of formules) {
const carte = modele.content.cloneNode(true); // copie du contenu du moule
carte.querySelector(".nom").textContent = formule.nom;
carte.querySelector(".prix").textContent = formule.prix;
carte.querySelector(".description").textContent = formule.description;
zone.append(carte);
}
selection.
const pieces = [
{ designation: "SSD NVMe 1 To", quantite: 3, prix: 74.9 },
{ designation: "RAM DDR5 16 Go", quantite: 2, prix: 52 },
{ designation: "Alimentation 650 W", quantite: 1, prix: 89 },
{ designation: "Pâte thermique", quantite: 5, prix: 7.5 },
];
// À toi d'écrire la suite…
const pieces = [
{ designation: "SSD NVMe 1 To", quantite: 3, prix: 74.9 },
{ designation: "RAM DDR5 16 Go", quantite: 2, prix: 52 },
{ designation: "Alimentation 650 W", quantite: 1, prix: 89 },
{ designation: "Pâte thermique", quantite: 5, prix: 7.5 },
];
const euros = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" });
const corps = document.querySelector("#lignes");
const lignes = pieces.map(({ designation, quantite, prix }) => {
const tr = document.createElement("tr");
for (const valeur of [designation, quantite, euros.format(prix), euros.format(prix * quantite)]) {
const td = document.createElement("td");
td.textContent = valeur;
tr.append(td);
}
tr.classList.toggle("selection", quantite > 1);
return tr;
});
corps.append(...lignes);
const total = pieces.reduce((somme, p) => somme + p.prix * p.quantite, 0);
document.querySelector("#total").textContent = euros.format(total);
classList.toggle("selection", quantite > 1) : la
version « forcée » de toggle, vue plus haut, évite un
if.
Un clic, une touche pressée, un formulaire envoyé, une page qui finit de charger : chacune de ces actions déclenche un événement. JavaScript peut s'y abonner et exécuter une fonction à chaque fois qu'il se produit. C'est ce qu'on appelle la programmation événementielle.
Pense à la sonnette de l'atelier : on ne reste pas planté devant la porte toute la journée. On installe la sonnette une fois, on vaque à ses occupations, et quand elle sonne, on va ouvrir. En JavaScript : on installe un écouteur (listener), et le navigateur appelle notre fonction quand l'événement survient.
const bouton = document.querySelector("#btn-devis");
const message = document.querySelector("#message");
let demandes = 0;
bouton.addEventListener("click", () => {
demandes++;
message.textContent = `Demande n° ${demandes} enregistrée, nous vous rappelons sous 24 h.`;
console.log("Clic reçu !", demandes);
});
addEventListener(type, fonction) prend le nom de
l'événement (sans « on ») et la fonction à appeler : un
callback, comme au chapitre 11. Clique plusieurs fois sur
le bouton du rendu : la fonction s'exécute à chaque clic.
Tu connais l'attribut HTML onclick (chapitre 02). Il existe
aussi la propriété onclick de l'élément. Son
défaut : elle ne peut contenir qu'une seule fonction, la deuxième
affectation écrase la première.
const bouton = document.querySelector("#btn-devis");
bouton.onclick = () => console.log("Propriété : fonction 1");
bouton.onclick = () => console.log("Propriété : fonction 2"); // écrase la 1 !
bouton.addEventListener("click", () => console.log("Écouteur A"));
bouton.addEventListener("click", () => console.log("Écouteur B")); // s'ajoute
console.log("Clique sur le bouton et compte les messages.");
| Écriture | Plusieurs fonctions ? | Verdict |
|---|---|---|
<button onclick="…"> | non, et le code est mêlé au HTML | à éviter |
bouton.onclick = fonction | non, la dernière écrase les autres | à éviter |
bouton.addEventListener("click", fonction) | oui, autant qu'on veut | à utiliser |
La fonction appelée reçoit automatiquement un argument : un objet qui
décrit l'événement en détail. On le nomme en général event,
evenement ou simplement e :
const zone = document.querySelector("#zone");
zone.addEventListener("click", (evenement) => {
console.log("Type :", evenement.type);
console.log("Cible :", evenement.target);
console.log("Position dans la zone :", evenement.offsetX, evenement.offsetY);
console.log("Touche Ctrl enfoncée :", evenement.ctrlKey);
});
const zone = document.querySelector("#zone");
const coordonnees = document.querySelector("#coordonnees");
zone.addEventListener("mouseenter", () => { zone.style.background = "#ffedd5"; });
zone.addEventListener("mouseleave", () => { zone.style.background = ""; });
zone.addEventListener("mousemove", (e) => {
coordonnees.textContent = `Position : ${e.offsetX} × ${e.offsetY} px`;
});
zone.addEventListener("dblclick", () => console.log("Double-clic !"));
zone.addEventListener("contextmenu", (e) => {
e.preventDefault(); // empêche le menu du navigateur de s'ouvrir
console.log("Clic droit intercepté.");
});
preventDefault() annule le comportement par
défaut du navigateur pour cet événement : ici l'ouverture du menu
contextuel. Tu le retrouveras pour les liens et surtout les formulaires.
mousemove se déclenche des dizaines de fois par seconde.
C'est pour ça que l'exemple met à jour un paragraphe de la page plutôt
que d'écrire dans la console, qui serait instantanément inondée. Garde
le code de ce genre d'écouteur très léger.
const zoneTexte = document.querySelector("#description");
const compteur = document.querySelector("#compteur");
// input : à chaque modification du contenu (frappe, collage, dictée…)
zoneTexte.addEventListener("input", () => {
const longueur = zoneTexte.value.length;
compteur.textContent = `${longueur} / 120 caractères`;
compteur.classList.toggle("alerte", longueur > 100);
});
// keydown : à chaque touche enfoncée
zoneTexte.addEventListener("keydown", (e) => {
console.log(`Touche : "${e.key}" (code physique : ${e.code})`);
if (e.key === "Enter" && e.ctrlKey) {
e.preventDefault();
console.log("📨 Ctrl + Entrée : description envoyée !");
}
});
e.key donne la touche telle qu'elle est comprise ("a", "A", "Enter", "ArrowUp") : à utiliser dans 99 % des cas ;e.code donne la touche physique ("KeyQ" pour la touche A d'un clavier français !) : utile pour les jeux ;e.ctrlKey, e.shiftKey, e.altKey, e.metaKey indiquent les touches de modification enfoncées.alerte.
Bonus : la touche Entrée dans le champ fait la même chose que
le bouton.
// À toi d'écrire le code…
const champ = document.querySelector("#prenom");
const bouton = document.querySelector("#saluer");
const salutation = document.querySelector("#salutation");
function saluer() {
const prenom = champ.value.trim();
salutation.classList.toggle("alerte", prenom === "");
salutation.textContent = prenom
? `Bonjour ${prenom}, bienvenue à l'atelier !`
: "Indiquez votre prénom.";
}
bouton.addEventListener("click", saluer);
champ.addEventListener("keydown", (e) => {
if (e.key === "Enter") saluer();
});
La fonction saluer est écrite une seule fois et
branchée sur deux événements : on passe son
nom (sans parenthèses !) à
addEventListener.
L'événement submit se déclenche sur le
<form> quand on l'envoie (bouton d'envoi ou touche
Entrée). Par défaut, le navigateur envoie alors les données et
recharge la page : on l'en empêche avec preventDefault() pour
traiter le formulaire en JavaScript. FormData récupère tous
les champs d'un coup, grâce à leur attribut name :
const formulaire = document.querySelector("#contact");
const retour = document.querySelector("#retour");
formulaire.addEventListener("submit", (e) => {
e.preventDefault(); // pas d'envoi, pas de rechargement
const donnees = Object.fromEntries(new FormData(formulaire));
console.log(donnees);
const erreurs = [];
if (donnees.nom.trim() === "") erreurs.push("le nom");
if (!donnees.email.includes("@")) erreurs.push("une adresse e-mail valide");
if (erreurs.length > 0) {
retour.className = "alerte";
retour.textContent = `Merci d'indiquer ${erreurs.join(" et ")}.`;
return;
}
retour.className = "succes";
retour.textContent = `Merci ${donnees.nom}, votre demande « ${donnees.service} » est bien reçue.`;
formulaire.reset();
});
// change : quand une valeur est validée (choix dans une liste, case cochée…)
document.querySelector("#service").addEventListener("change", (e) => {
console.log("Prestation choisie :", e.target.value);
});
focus
et blur (un champ prend ou perd le focus), input
(à chaque frappe) et reset. Et une vérification en JavaScript
ne remplace jamais celle du serveur : un visiteur malin peut toujours
contourner le code de la page.
// Le HTML est entièrement lu et le DOM construit (images pas forcément chargées)
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM prêt : on peut chercher les éléments.");
});
// Tout est chargé : images, feuilles de style, polices…
window.addEventListener("load", () => {
console.log("Page entièrement chargée.");
});
Dans d'anciens scripts placés dans le <head>, tout le
code était enveloppé dans DOMContentLoaded pour attendre que
les éléments existent. Avec defer (chapitre 02) ou
type="module", le script s'exécute déjà une fois le DOM prêt :
plus besoin de cet emballage. load reste utile quand on a
besoin des dimensions réelles des images.
Quand on clique sur un élément, l'événement ne s'arrête pas à lui : il
remonte ensuite vers son parent, puis le parent du parent,
jusqu'à document. C'est le bouillonnement
(bubbling), comme une bulle qui remonte à la surface.
document.querySelector("#carte").addEventListener("click", () => console.log("3. la carte"));
document.querySelector("#services").addEventListener("click", () => console.log("2. la liste"));
for (const bouton of document.querySelectorAll("#services button")) {
bouton.addEventListener("click", (e) => {
console.log(`1. le bouton « ${e.target.textContent} »`);
});
}
console.log("Clique sur un bouton, puis dans la carte en dehors des boutons.");
e.stopPropagation() stoppe cette remontée, mais on en a
rarement besoin. Au contraire, le bouillonnement permet une technique
précieuse : la délégation.
Plutôt que de poser un écouteur sur chaque bouton, on en pose
un seul sur leur parent commun. Grâce au bouillonnement,
il reçoit tous les clics de ses descendants ; e.target indique
l'élément réellement cliqué, et closest() retrouve celui qui
nous intéresse. Gros avantage : ça marche aussi pour les éléments
ajoutés plus tard.
const panier = document.querySelector("#panier");
// UN seul écouteur pour tous les boutons « Retirer », présents et futurs
panier.addEventListener("click", (e) => {
const bouton = e.target.closest('[data-action="retirer"]');
if (!bouton) return; // clic ailleurs dans la liste : on ignore
const ligne = bouton.closest("li");
console.log("Retiré :", ligne.firstChild.textContent.trim());
ligne.remove();
});
document.querySelector("#ajouter").addEventListener("click", () => {
panier.insertAdjacentHTML("beforeend",
'<li>Ventirad <button type="button" class="secondaire" data-action="retirer">Retirer</button></li>');
});
removeEventListener(type, fonction) désabonne une fonction. Il
faut lui passer exactement la même fonction : une fonction
nommée, donc, pas une fonction fléchée écrite sur place. Et pour un
écouteur qui ne doit servir qu'une fois, l'option once fait
le travail toute seule :
const bouton = document.querySelector("#btn-devis");
const message = document.querySelector("#message");
let clics = 0;
function compterClics() {
clics++;
message.textContent = `Clic ${clics} sur 3`;
if (clics === 3) {
bouton.removeEventListener("click", compterClics); // même fonction
message.textContent = "Limite atteinte : le bouton ne réagit plus.";
}
}
bouton.addEventListener("click", compterClics);
bouton.addEventListener("click", () => console.log("Premier clic : bienvenue !"), { once: true });
| Famille | Événements |
|---|---|
| Souris et pointeur | click, dblclick, contextmenu, mouseenter, mouseleave, mousemove, pointerdown, pointerup |
| Clavier | keydown, keyup |
| Formulaires | submit, reset, input, change, focus, blur |
| Page et fenêtre | DOMContentLoaded, load, resize, scroll, beforeunload |
| Presse-papiers | copy, cut, paste |
| Médias | play, pause, ended, timeupdate |
<button>
(ou un lien <a> pour naviguer), jamais une
<div> avec un écouteur click. Le bouton est
atteignable au clavier avec Tab, s'active avec
Entrée ou Espace, et il est annoncé correctement par
les lecteurs d'écran. La <div>, elle, n'est rien de tout
ça.
Trois petits projets pour mettre en œuvre tout ce que tu viens d'apprendre. Chacun a sa page de démonstration : écris ton code, clique sur Exécuter, puis teste dans le rendu.
mouseenter, déplace-le à une position aléatoire de la zone
(propriétés left et top, le bouton est en
position: absolute). Le bouton « Garder mon rendez-vous »,
lui, affiche un message de remerciement.
// À toi d'écrire le code…
const terrain = document.querySelector("#terrain");
const fuyard = document.querySelector("#fuyard");
let fuites = 0;
fuyard.addEventListener("mouseenter", () => {
const maxX = terrain.clientWidth - fuyard.offsetWidth;
const maxY = terrain.clientHeight - fuyard.offsetHeight;
fuyard.style.left = `${Math.floor(Math.random() * maxX)}px`;
fuyard.style.top = `${Math.floor(Math.random() * maxY)}px`;
fuites++;
console.log(`Raté ! (${fuites})`);
});
document.querySelector("#garder").addEventListener("click", () => {
document.querySelector("#reponse").textContent = "Merci, à très bientôt à l'atelier ! 😄";
});
clientWidth et offsetWidth donnent les
dimensions réelles en pixels : on retire la taille du bouton pour
qu'il ne sorte jamais de la zone. Au clavier, le bouton reste
atteignable avec Tab… preuve qu'un vrai
<button> est toujours accessible !
faite, qui la barre), et le
bouton ✕ la supprime. Le compteur indique le nombre de tâches restantes.
Utilise la délégation pour la liste.
// À toi d'écrire le code…
const formulaire = document.querySelector("#nouvelle");
const liste = document.querySelector("#taches");
const restantes = document.querySelector("#restantes");
function ajouterTache(texte) {
const li = document.createElement("li");
const span = document.createElement("span");
span.className = "texte";
span.textContent = texte;
const bouton = document.createElement("button");
bouton.type = "button";
bouton.className = "supprimer secondaire";
bouton.textContent = "✕";
bouton.setAttribute("aria-label", `Supprimer « ${texte} »`);
li.append(span, bouton);
liste.append(li);
mettreAJourCompteur();
}
function mettreAJourCompteur() {
const n = liste.querySelectorAll("li:not(.faite)").length;
restantes.textContent = `${n} tâche${n > 1 ? "s" : ""} restante${n > 1 ? "s" : ""}`;
}
formulaire.addEventListener("submit", (e) => {
e.preventDefault();
const champ = formulaire.elements.tache;
ajouterTache(champ.value.trim());
champ.value = "";
champ.focus();
});
liste.addEventListener("click", (e) => {
const li = e.target.closest("li");
if (!li) return;
if (e.target.closest(".supprimer")) {
li.remove();
} else if (e.target.closest(".texte")) {
li.classList.toggle("faite");
}
mettreAJourCompteur();
});
["Commander la pâte thermique", "Rappeler Mme Le Gall", "Tester l'alimentation du PC de Yann"].forEach(ajouterTache);
L'attribut required du champ empêche l'envoi d'une
tâche vide sans une ligne de JavaScript. Pour aller plus loin,
sauvegarde la liste dans localStorage (en JSON) à chaque
changement, et recharge-la au démarrage.
ouvert), met à jour l'attribut aria-expanded
(pour les lecteurs d'écran) et change le libellé en « ✕ Fermer ». La touche
Échap referme le menu.
// À toi d'écrire le code…
const bouton = document.querySelector("#bouton-menu");
const navigation = document.querySelector("#navigation");
function basculerMenu(ouvrir) {
navigation.classList.toggle("ouvert", ouvrir);
bouton.setAttribute("aria-expanded", String(ouvrir));
bouton.textContent = ouvrir ? "✕ Fermer" : "☰ Menu";
}
bouton.addEventListener("click", () => {
basculerMenu(!navigation.classList.contains("ouvert"));
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") basculerMenu(false);
});
navigation.addEventListener("click", (e) => {
if (e.target.closest("a")) {
e.preventDefault(); // démo : pas de vraie navigation
console.log("Lien choisi :", e.target.textContent);
basculerMenu(false);
}
});
Toute la logique tient dans une fonction
basculerMenu(ouvrir), appelée par les trois écouteurs.
Sur un vrai site, une media query (cours CSS) afficherait le menu
en permanence sur grand écran et cacherait le bouton.
Tes pages sont vivantes ! 🎉 Tu sais maintenant :
setTimeout et setInterval ;localStorage ;querySelector et naviguer dans l'arbre du DOM ;innerHTML ;createElement ou un <template> ;// Tout ce que tu sais faire, en quelques lignes
const CLE = "ai29-cours-js-theme";
const caseSombre = document.querySelector("#sombre");
const zone = document.querySelector("#zone-bilan");
const etat = document.querySelector("#etat");
function appliquerTheme(sombre) {
zone.classList.toggle("sombre", sombre);
caseSombre.checked = sombre;
etat.textContent = sombre ? "Thème sombre activé." : "Thème clair.";
}
let memorise = null;
try { memorise = localStorage.getItem(CLE); } catch { /* stockage indisponible */ }
appliquerTheme(memorise === "sombre");
caseSombre.addEventListener("change", () => {
appliquerTheme(caseSombre.checked);
try { localStorage.setItem(CLE, caseSombre.checked ? "sombre" : "clair"); } catch { /* idem */ }
console.log("Préférence enregistrée :", caseSombre.checked ? "sombre" : "clair");
});
Coche la case, puis recharge la page : le thème choisi est retrouvé. Le
bloc try { … } catch { … } protège le code au cas où le
stockage serait bloqué (navigation privée stricte, par exemple) : il fait
partie du programme de la partie 5, consacrée à
l'asynchrone (attendre une réponse du réseau sans bloquer la page), aux
dates, aux modules, et à un projet final qui rassemble tout le cours.
Certaines opérations prennent du temps : interroger un serveur, lire un fichier, attendre un minuteur. Si JavaScript restait planté à attendre leur réponse, toute la page se figerait : plus de clic, plus de défilement. Il les traite donc de façon asynchrone : il lance l'opération, continue son travail, et s'occupe du résultat quand il arrive.
C'est exactement ce que fait l'atelier quand il commande une pièce : on passe la commande au fournisseur, puis on continue les autres réparations. On ne reste pas devant la porte à guetter le livreur. Quand le colis arrive, on reprend la réparation en attente.
JavaScript n'exécute qu'une seule chose à la fois. Les tâches asynchrones terminées attendent dans une file, et le moteur ne les traite qu'une fois le code en cours terminé. Ce mécanisme s'appelle la boucle d'événements (event loop). Devine l'ordre d'affichage avant de regarder la console :
console.log("1. Début de la journée");
setTimeout(() => console.log("4. Le minuteur de 0 ms"), 0);
Promise.resolve().then(() => console.log("3. La promesse déjà tenue"));
console.log("2. Fin du code principal");
Même avec un délai de 0 ms, le minuteur passe après le code principal : il doit attendre son tour dans la file. Et les promesses (qu'on va découvrir) ont une file prioritaire, traitée avant celle des minuteurs.
La première façon de gérer l'asynchrone, tu la connais : le callback, une fonction appelée quand l'opération est finie (chapitre 11). Le problème apparaît quand plusieurs opérations doivent s'enchaîner, chacune attendant la précédente :
console.log("Réception du PC…");
setTimeout(() => {
console.log("✔ Diagnostic terminé : SSD défectueux");
setTimeout(() => {
console.log("✔ SSD de remplacement livré");
setTimeout(() => {
console.log("✔ SSD monté et système restauré");
setTimeout(() => {
console.log("✔ Client prévenu : PC prêt !");
}, 400);
}, 400);
}, 400);
}, 400);
Le code dérive vers la droite à chaque étape : c'est la célèbre pyramide des callbacks (callback hell). Ajoute la gestion d'une erreur à chaque niveau, et il devient illisible. Les promesses ont été inventées pour remettre ce code à plat.
Une promesse (Promise) est un objet qui
représente un résultat à venir. Comme un bon de commande : on ne
tient pas encore la pièce, mais on a un document qui garantit qu'on saura
ce qu'elle devient. Une promesse a trois états possibles :
| État | Signification | Pour la commande |
|---|---|---|
| pending (en attente) | l'opération est en cours | commande passée, colis en route |
| fulfilled (tenue) | l'opération a réussi, avec une valeur | pièce livrée |
| rejected (rompue) | l'opération a échoué, avec une erreur | rupture de stock |
Pour créer une promesse, on donne au constructeur une
fonction qui reçoit deux outils : resolve (tenir la promesse
avec une valeur) et reject (la rompre avec une erreur). Pour
l'utiliser, on branche .then() (en cas de
succès), .catch() (en cas d'échec) et
.finally() (dans tous les cas) :
function commanderPiece(piece) {
return new Promise((resolve, reject) => {
setTimeout(() => { // on simule le délai du fournisseur
if (piece.includes("RTX 5090")) {
reject(new Error(`Rupture de stock : ${piece}`));
} else {
resolve(`${piece} livré`);
}
}, 600);
});
}
const commande = commanderPiece("SSD 1 To");
console.log(commande); // une promesse, encore en attente
commande
.then(resultat => console.log("✔", resultat))
.catch(erreur => console.error("✖", erreur.message))
.finally(() => console.log("Suivi de commande mis à jour."));
commanderPiece("Carte graphique RTX 5090")
.then(resultat => console.log("✔", resultat))
.catch(erreur => console.error("✖", erreur.message));
.then() renvoie lui-même une promesse. Si son callback renvoie
une nouvelle promesse, le .then() suivant attend qu'elle soit
tenue. On peut ainsi écrire une suite d'étapes à plat, avec
un seul .catch() pour toutes les erreurs :
const etape = (message, delai = 400) =>
new Promise(resolve => setTimeout(() => resolve(message), delai));
console.log("Réception du PC…");
etape("✔ Diagnostic terminé : SSD défectueux")
.then(message => { console.log(message); return etape("✔ SSD de remplacement livré"); })
.then(message => { console.log(message); return etape("✔ SSD monté et système restauré"); })
.then(message => { console.log(message); return etape("✔ Client prévenu : PC prêt !"); })
.then(message => console.log(message))
.catch(erreur => console.error("Problème en cours de route :", erreur.message));
Quand plusieurs opérations sont indépendantes, inutile de
les attendre l'une après l'autre : on les lance toutes en même temps.
Quatre méthodes statiques de Promise permettent de combiner
leurs résultats :
// Chaque fournisseur répond plus ou moins vite, avec son prix pour un SSD 1 To
const interroger = (fournisseur, prix, delai, enStock = true) =>
new Promise((resolve, reject) => setTimeout(() => {
enStock ? resolve({ fournisseur, prix }) : reject(new Error(`${fournisseur} : rupture`));
}, delai));
const demandes = () => [
interroger("Grossiste Brest", 79.9, 500),
interroger("Distributeur Rennes", 74.5, 300),
interroger("Site en ligne", 71, 700, false),
];
Promise.allSettled(demandes()).then(resultats => {
console.log("allSettled : tous les résultats, succès ou échec");
console.table(resultats.map(r => r.status === "fulfilled" ? r.value : { erreur: r.reason.message }));
});
Promise.any(demandes()).then(premier => {
console.log("any : la première réponse positive →", premier.fournisseur);
});
Promise.all(demandes())
.then(() => console.log("all : tout a réussi"))
.catch(erreur => console.warn("all : échoue dès qu'une seule échoue →", erreur.message));
| Méthode | Tenue quand… | Rompue quand… |
|---|---|---|
Promise.all | toutes sont tenues (tableau des valeurs) | une seule est rompue |
Promise.allSettled | toutes sont terminées, d'une façon ou d'une autre | jamais |
Promise.any | une première est tenue | toutes sont rompues |
Promise.race | suit la toute première à se terminer, succès ou échec (pratique pour un délai maximal) | |
Les promesses ont aplati la pyramide, mais les .then() en
cascade restent verbeux. Depuis 2017, deux mots-clés permettent d'écrire du
code asynchrone comme du code ordinaire, ligne après
ligne :
async devant une fonction indique qu'elle est asynchrone : elle renvoie toujours une promesse ;await devant une promesse met la fonction en pause jusqu'à ce que la promesse soit tenue, et donne directement sa valeur. Le reste de la page, lui, continue de vivre.const etape = (message, delai = 400) =>
new Promise(resolve => setTimeout(() => resolve(message), delai));
async function reparerPc() {
console.log("Réception du PC…");
console.log(await etape("✔ Diagnostic terminé : SSD défectueux"));
console.log(await etape("✔ SSD de remplacement livré"));
console.log(await etape("✔ SSD monté et système restauré"));
return "PC prêt !";
}
reparerPc().then(bilan => console.log("🎉", bilan));
console.log("(Pendant ce temps, l'atelier continue de tourner.)");
Pour les erreurs, on retrouve un outil classique : le bloc
try…catch. Une promesse rompue pendant un await
se comporte comme une erreur levée à cette ligne :
function commanderPiece(piece) {
return new Promise((resolve, reject) => setTimeout(() => {
piece.includes("RTX 5090") ? reject(new Error(`Rupture : ${piece}`)) : resolve(`${piece} livré`);
}, 400));
}
async function preparerCommande(pieces) {
for (const piece of pieces) {
try {
const resultat = await commanderPiece(piece);
console.log("✔", resultat);
} catch (erreur) {
console.warn("✖", erreur.message, "→ on propose une alternative au client");
}
}
console.log("Commande traitée.");
}
preparerCommande(["SSD 1 To", "Carte graphique RTX 5090", "RAM 32 Go"]);
await dans une boucle attend chaque opération avant de
lancer la suivante. Si elles sont indépendantes, lance-les toutes puis
attends-les ensemble avec Promise.all :
const verifierStock = (piece) =>
new Promise(resolve => setTimeout(() => resolve(`${piece} : en stock`), 500));
const pieces = ["SSD", "RAM", "Alimentation"];
let debut = Date.now();
for (const piece of pieces) {
await verifierStock(piece); // l'une après l'autre
}
console.log(`En série : ≈ ${Math.round((Date.now() - debut) / 100) * 100} ms`);
debut = Date.now();
const resultats = await Promise.all(pieces.map(verifierStock)); // toutes en même temps
console.log(`En parallèle : ≈ ${Math.round((Date.now() - debut) / 100) * 100} ms`);
console.log(resultats);
await directement, hors de toute
fonction async. C'est autorisé dans la console du navigateur,
dans les modules (chapitre 23) et dans les exemples de ce cours. Dans un
script classique, await doit se trouver dans une fonction
async.
try…catch ne sert pas qu'à l'asynchrone. Il permet de
rattraper n'importe quelle erreur pour qu'elle n'arrête pas
le programme : on essaie un bloc de code, et en cas d'erreur, on
exécute le bloc catch à la place. Le bloc
finally, facultatif, s'exécute dans tous les cas.
const saisies = ['{"client": "Yann", "pieces": 2}', "{client: Yann}"];
for (const texte of saisies) {
try {
const fiche = JSON.parse(texte); // peut lever une erreur
console.log("Fiche lue :", fiche);
} catch (erreur) {
console.warn(`Saisie illisible (${erreur.name}) : ${texte}`);
} finally {
console.log("— saisie suivante —");
}
}
On peut aussi lever ses propres erreurs avec throw, et même
créer ses propres types d'erreur en étendant la classe Error
(chapitre 16), pour les distinguer dans le catch :
class ErreurStock extends Error {
constructor(piece) {
super(`Plus de « ${piece} » en stock`);
this.name = "ErreurStock";
this.piece = piece;
}
}
function retirerDuStock(stock, piece) {
if (!stock[piece]) throw new ErreurStock(piece);
stock[piece]--;
return stock[piece];
}
const stock = { "SSD 1 To": 1, "RAM 16 Go": 0 };
for (const piece of ["SSD 1 To", "SSD 1 To", "RAM 16 Go"]) {
try {
console.log(`${piece} : il en reste ${retirerDuStock(stock, piece)}`);
} catch (erreur) {
if (erreur instanceof ErreurStock) {
console.warn(`${erreur.message} → commande fournisseur à prévoir`);
} else {
throw erreur; // une erreur inattendue : on ne la masque pas
}
}
}
catch vide qui avale l'erreur sans rien faire. Le
programme continue, mais dans un état faux, et plus personne ne sait
pourquoi. Rattrape une erreur seulement si tu sais quoi en faire (prévenir
le visiteur, proposer une alternative, réessayer) ; sinon, laisse-la
remonter.
Voici l'usage n° 1 de l'asynchrone : fetch() envoie une
requête HTTP et renvoie une promesse de réponse. La
réponse arrive en deux temps : d'abord les en-têtes (avec le code de
statut), puis le contenu, qu'on lit avec .json() (une autre
promesse). Exemple avec un petit fichier JSON
(tarifs-atelier.json) déposé sur
ce site :
try {
const reponse = await fetch("../data/tarifs-atelier.json");
if (!reponse.ok) {
throw new Error(`Le serveur a répondu ${reponse.status}`);
}
const tarifs = await reponse.json();
console.log(`Tarifs de ${tarifs.atelier} (${tarifs.ville}) :`);
console.table(tarifs.prestations);
} catch (erreur) {
console.error("Chargement impossible :", erreur.message);
}
reponse.ok (vrai pour un statut entre 200 et 299). Autre
détail : si tu ouvres une page de cours directement depuis ton disque
(file://), le navigateur bloque fetch par
sécurité. Utilise un petit serveur local (l'extension Live
Server de VS Code, par exemple).
Et avec une vraie API publique ? L'État met à disposition
geo.api.gouv.fr, qui renvoie les informations des communes
françaises. Parfait pour vérifier qu'une commune se trouve dans la zone
d'intervention de l'atelier. Tape un nom de commune dans le rendu :
const formulaire = document.querySelector("#recherche");
const statut = document.querySelector("#statut");
const liste = document.querySelector("#resultats");
const DEPARTEMENTS_DESSERVIS = ["29", "22"]; // Finistère et Côtes-d'Armor
async function rechercherCommunes(nom) {
const adresse = new URL("https://geo.api.gouv.fr/communes");
adresse.searchParams.set("nom", nom);
adresse.searchParams.set("fields", "nom,codesPostaux,codeDepartement,population");
adresse.searchParams.set("boost", "population");
adresse.searchParams.set("limit", "6");
const reponse = await fetch(adresse);
if (!reponse.ok) throw new Error(`Erreur ${reponse.status}`);
return reponse.json();
}
formulaire.addEventListener("submit", async (e) => {
e.preventDefault();
statut.textContent = "Recherche en cours…";
liste.replaceChildren();
try {
const communes = await rechercherCommunes(formulaire.elements.commune.value.trim());
console.log(communes);
statut.textContent = `${communes.length} commune(s) trouvée(s).`;
for (const c of communes) {
const li = document.createElement("li");
const dedans = DEPARTEMENTS_DESSERVIS.includes(c.codeDepartement);
li.textContent = `${c.nom} (${c.codesPostaux[0]}) — ${c.population?.toLocaleString("fr-FR") ?? "?"} hab.`;
const badge = document.createElement("span");
badge.className = `zone ${dedans ? "dedans" : "dehors"}`;
badge.textContent = dedans ? "zone desservie" : "hors zone";
li.append(badge);
liste.append(li);
}
} catch (erreur) {
statut.textContent = `Impossible de joindre le service : ${erreur.message}`;
statut.className = "alerte";
}
});
fetch sait aussi envoyer des données : il
suffit de préciser la méthode, les en-têtes et le contenu. C'est ainsi
qu'un formulaire de contact moderne transmet sa demande sans recharger la
page (ce code est un modèle, aucun serveur ne l'attend ici) :
const reponse = await fetch("https://client.ai29.tech/api/demandes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nom: "Yann", service: "devis", message: "PC très lent" }),
});
if (!reponse.ok) throw new Error(`Envoi refusé : ${reponse.status}`);
const accuse = await reponse.json();
attendre(ms) qui renvoie une
promesse tenue au bout de ms millisecondes. Utilise-la pour
simuler une sauvegarde de données en trois étapes (« Copie des documents »,
« Copie des photos », « Vérification »), chacune affichée après 500 ms,
puis affiche la durée totale. Si une variable disqueBranche
vaut false, lève une erreur dès le départ et affiche-la
proprement avec try…catch.
const attendre = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function sauvegarder(disqueBranche) {
if (!disqueBranche) {
throw new Error("Aucun disque de sauvegarde détecté.");
}
const debut = Date.now();
for (const etape of ["Copie des documents", "Copie des photos", "Vérification"]) {
await attendre(500);
console.log(`✔ ${etape}`);
}
return Date.now() - debut;
}
for (const disqueBranche of [false, true]) {
try {
const duree = await sauvegarder(disqueBranche);
console.log(`Sauvegarde terminée en ≈ ${Math.round(duree / 100) / 10} s`);
} catch (erreur) {
console.error(`Sauvegarde impossible : ${erreur.message}`);
}
}
Date de dépôt d'un appareil, fin de garantie, créneau de rendez-vous,
validité d'un devis : un logiciel d'atelier manipule des dates en
permanence. JavaScript fournit pour ça l'objet Date, puissant
mais truffé de petits pièges qu'il vaut mieux connaître.
const maintenant = new Date(); // date et heure actuelles
console.log(maintenant);
const depot = new Date(2026, 10, 4, 14, 30); // année, mois, jour, heure, minute
console.log(depot);
const rendezVous = new Date("2026-11-12T09:30"); // texte au format ISO
console.log(rendezVous);
const origine = new Date(0); // nombre de millisecondes depuis le 1er janvier 1970
console.log(origine);
new Date(2026, 10, 4), le 10 désigne
novembre : janvier vaut 0, décembre vaut 11. Les jours du
mois, eux, commencent bien à 1. Pour éviter toute confusion, préfère le
format texte ISO "AAAA-MM-JJ", où les mois sont numérotés
normalement.
const depot = new Date("2026-11-04T14:30");
const JOURS = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"];
console.log("Année :", depot.getFullYear());
console.log("Mois :", depot.getMonth() + 1); // + 1 pour retrouver le numéro habituel
console.log("Jour du mois :", depot.getDate());
console.log("Jour de la semaine :", depot.getDay(), "→", JOURS[depot.getDay()]); // 0 = dimanche
console.log("Heure :", depot.getHours(), "h", depot.getMinutes());
getDate() donne le jour du mois (4), alors que
getDay() donne le jour de la semaine (0 pour
dimanche, 1 pour lundi…). Et getYear(), une ancienne méthode,
renvoie l'année moins 1900 : utilise toujours
getFullYear().
Les méthodes set…() modifient une date, et elles gèrent
automatiquement les débordements : ajouter 30 jours au 15 novembre donne
bien le 15 décembre. Comme ces méthodes modifient la date sur place, on
travaille sur une copie :
function ajouterJours(date, jours) {
const resultat = new Date(date); // copie : l'original reste intact
resultat.setDate(resultat.getDate() + jours);
return resultat;
}
const devis = new Date("2026-11-15");
console.log("Devis du :", devis.toLocaleDateString("fr-FR"));
console.log("Valable jusqu'au :", ajouterJours(devis, 30).toLocaleDateString("fr-FR"));
console.log("Le 31 décembre + 1 jour :", ajouterJours(new Date("2026-12-31"), 1).toLocaleDateString("fr-FR"));
const finDeGarantie = new Date("2026-03-10");
finDeGarantie.setFullYear(finDeGarantie.getFullYear() + 2); // garantie de 2 ans
console.log("Fin de garantie :", finDeGarantie.toLocaleDateString("fr-FR"));
Pour l'écart entre deux dates, on soustrait l'une de l'autre : le résultat est un nombre de millisecondes, qu'on convertit ensuite :
const MS_PAR_JOUR = 24 * 60 * 60 * 1000;
const depot = new Date("2026-11-04");
const restitution = new Date("2026-11-13");
const ecart = restitution - depot;
console.log(ecart, "ms");
console.log(Math.round(ecart / MS_PAR_JOUR), "jours d'immobilisation");
Comme Intl.NumberFormat pour les prix (chapitre 07),
toLocaleDateString(), toLocaleTimeString() et
Intl.DateTimeFormat formatent une date selon les usages d'une
langue, avec de nombreuses options :
const rdv = new Date("2026-11-12T09:30");
console.log(rdv.toLocaleDateString("fr-FR"));
console.log(rdv.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" }));
console.log(rdv.toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long", year: "numeric" }));
console.log(rdv.toLocaleString("fr-FR", { dateStyle: "full", timeStyle: "short" }));
const court = new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "short" });
console.log(court.format(rdv));
console.log(rdv.toISOString()); // format international, en heure UTC : idéal pour stocker
Et pour des formules comme « dans 3 jours » ou « il y a 2 semaines »,
Intl.RelativeTimeFormat fait le travail, accords compris :
const relatif = new Intl.RelativeTimeFormat("fr-FR", { numeric: "auto" });
console.log(relatif.format(1, "day"));
console.log(relatif.format(3, "day"));
console.log(relatif.format(-1, "day"));
console.log(relatif.format(-2, "week"));
console.log(relatif.format(1, "month"));
console.log(new Date("2026-11-04T00:00")); // ✓ format ISO : fiable partout
console.log(new Date("04/11/2026")); // ✗ compris comme le 11 avril (format américain !)
console.log(new Date("31/12/2026")); // ✗ Invalid Date : il n'y a pas de 31e mois…
// Une date française saisie par un client : on la découpe soi-même
const saisie = "04/11/2026";
const [jour, mois, annee] = saisie.split("/").map(Number);
const date = new Date(annee, mois - 1, jour); // mois - 1 : le fameux piège !
console.log(date.toLocaleDateString("fr-FR", { dateStyle: "long" }));
new Date("2026-11-04") (une date seule) est lue en heure
universelle (UTC), alors que
new Date("2026-11-04T00:00") (avec une heure) est lue en heure
locale. En France, la première correspond donc à 1 h du
matin (2 h en été), et peut même tomber la veille dans d'autres fuseaux.
Pour une date locale, ajoute toujours l'heure, ou passe par
new Date(annee, mois - 1, jour).
<input type="date"> renvoie toujours sa valeur
au format ISO "2026-11-04", quelle que soit la langue de
l'affichage. Le navigateur se charge de présenter un calendrier au format
local : aucune conversion à faire.
En interne, une date n'est qu'un nombre : les millisecondes écoulées depuis le 1er janvier 1970 à minuit (heure universelle). Ce nombre s'appelle un timestamp. Il est idéal pour stocker une date, comparer ou mesurer une durée :
console.log(Date.now()); // timestamp actuel
const depot = new Date("2026-11-04T14:30");
console.log(depot.getTime()); // date → timestamp
console.log(new Date(1793799000000).toLocaleString("fr-FR")); // timestamp → date
const debut = Date.now();
let total = 0;
for (let i = 0; i < 1_000_000; i++) total += i; // un calcul un peu long
console.log(`Calcul effectué en ${Date.now() - debut} ms`);
const a = new Date("2026-11-04");
const b = new Date("2026-11-04");
const c = new Date("2026-11-20");
console.log(a < c, c > a); // ✓ les comparaisons d'ordre fonctionnent
console.log(a === b); // ✗ false : deux objets différents !
console.log(a.getTime() === b.getTime()); // ✓ on compare les timestamps
=== compare des objets, donc des références
(chapitre 12) : deux dates identiques mais créées séparément ne sont pas
égales. Pour tester l'égalité, compare leurs getTime().
Date date de 1995 et ses défauts (mois à partir de 0, dates
modifiables sur place, fuseaux horaires mal gérés) sont connus de tous. Une
nouvelle API, Temporal, arrive progressivement dans les
navigateurs pour les corriger. En attendant qu'elle soit disponible
partout, Date et Intl restent les outils de base.
etatGarantie(dateAchat) qui reçoit une date
d'achat (texte ISO) et renvoie une phrase : « Sous garantie jusqu'au
10 mars 2028 (encore 492 jours) » ou « Garantie expirée depuis le
10 mars 2025 ». La garantie dure 2 ans. Teste-la sur trois dates d'achat.
Bonus : une fonction prochainJourOuvre(date) qui renvoie la
date elle-même si c'est un jour de semaine, ou le lundi suivant si elle
tombe un week-end.
const MS_PAR_JOUR = 24 * 60 * 60 * 1000;
const formatLong = new Intl.DateTimeFormat("fr-FR", { dateStyle: "long" });
function etatGarantie(dateAchat) {
const fin = new Date(dateAchat);
fin.setFullYear(fin.getFullYear() + 2);
const restant = Math.ceil((fin - Date.now()) / MS_PAR_JOUR);
return restant > 0
? `Sous garantie jusqu'au ${formatLong.format(fin)} (encore ${restant} jours)`
: `Garantie expirée depuis le ${formatLong.format(fin)}`;
}
function prochainJourOuvre(date) {
const resultat = new Date(date);
while (resultat.getDay() === 0 || resultat.getDay() === 6) {
resultat.setDate(resultat.getDate() + 1);
}
return resultat;
}
for (const achat of ["2023-03-10", "2025-06-01", "2026-10-20"]) {
console.log(`Achat du ${achat} : ${etatGarantie(achat)}`);
}
const samedi = new Date("2026-11-07");
console.log("Rendez-vous possible le :", formatLong.format(prochainJourOuvre(samedi)));
Un vrai site ne tient pas dans un seul fichier de 3 000 lignes. Les modules permettent de découper le code en fichiers spécialisés (les tarifs, le calcul du devis, l'affichage…), chacun choisissant ce qu'il partage avec les autres. C'est l'organisation de tous les projets JavaScript modernes.
Avec plusieurs balises <script> classiques, tous les
fichiers partagent le même espace global : une variable
total déclarée dans un fichier peut entrer en collision avec
une variable total d'un autre. Et l'ordre des balises devient
critique : un script qui utilise une fonction d'un autre fichier doit être
chargé après lui. Sur un gros projet, c'est ingérable.
Un module, lui, a sa propre portée : ce qu'il déclare reste privé, sauf ce qu'il exporte explicitement. Et un fichier qui a besoin d'un outil l'importe nommément : les dépendances sont écrites noir sur blanc.
Imaginons l'organisation du calculateur de devis du chapitre suivant :
scripts/
├── main.js ← point d'entrée, chargé par la page
├── tarifs.js ← les prix de l'atelier
├── devis.js ← la classe Devis et le calcul
└── outils/
└── format.js ← mise en forme des prix et des dates
Dans outils/format.js, on exporte des fonctions avec le mot-clé export :
// scripts/outils/format.js
const euros = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" });
const dateLongue = new Intl.DateTimeFormat("fr-FR", { dateStyle: "long" });
export function formaterPrix(montant) {
return euros.format(montant);
}
export function formaterDate(date) {
return dateLongue.format(date);
}
// « euros » et « dateLongue » ne sont pas exportés : ils restent privés au module
Un module peut aussi avoir un export par défaut, sa valeur principale :
// scripts/tarifs.js
export const TAUX_HORAIRE = 45;
export const PRIX_DEVIS = 20;
export default {
nettoyage: { libelle: "Nettoyage complet", heures: 1 },
systeme: { libelle: "Installation du système", heures: 1.5 },
};
Dans main.js, on importe ce dont on a besoin :
// scripts/main.js
import prestations, { TAUX_HORAIRE, PRIX_DEVIS } from "./tarifs.js"; // défaut + nommés
import { formaterPrix, formaterDate } from "./outils/format.js";
console.log(formaterPrix(TAUX_HORAIRE), formaterDate(new Date()));
console.log(prestations.nettoyage.libelle, PRIX_DEVIS);
| Écriture | Effet |
|---|---|
export function f() {}, export const X = 1; | export nommé (autant qu'on veut par fichier) |
export default valeur; | export par défaut (un seul par fichier) |
import { f, X } from "./fichier.js"; | importer des exports nommés (noms exacts, entre accolades) |
import nom from "./fichier.js"; | importer l'export par défaut, sous le nom de son choix |
import { f as formater } from "./fichier.js"; | renommer à l'import |
import * as outils from "./fichier.js"; | tout importer dans un objet : outils.f() |
export { f } from "./fichier.js"; | réexporter (pour regrouper plusieurs modules en un) |
"./tarifs.js" et non "./tarifs" ou
"tarifs.js". Le ./ indique un fichier relatif au
module qui importe.
Seul le point d'entrée est déclaré dans le HTML, avec type="module" ; il importe lui-même tous les autres :
<script type="module" src="scripts/main.js"></script>
Un script de type module se distingue d'un script classique sur plusieurs points :
defer : inutile de l'ajouter ;await y est autorisé directement, hors de toute fonction async.file://), une page avec
des modules ne fonctionne pas, le navigateur bloquant leur chargement par
sécurité. Pendant le développement, utilise un serveur local : l'extension
Live Server de VS Code, ou une commande comme
npx serve.
import(), utilisé comme une fonction, charge un module
à la demande et renvoie une promesse. Pratique pour ne
télécharger un outil lourd (un générateur de PDF, un éditeur…) qu'au moment
où le visiteur en a besoin. Pour le démontrer ici, on fabrique un
« fichier » module virtuel en mémoire :
// Le contenu d'un petit module, comme s'il était dans un fichier tarifs.js
const source = `
export const TAUX_HORAIRE = 45;
export function prixMainOeuvre(heures) { return heures * TAUX_HORAIRE; }
export default "Module tarifs, version 2026";
`;
const adresse = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
console.log("Chargement du module à la demande…");
const tarifs = await import(adresse); // l'import dynamique renvoie une promesse
console.log(tarifs.default);
console.log("2 h de main d'œuvre :", tarifs.prixMainOeuvre(2), "€");
console.log(Object.keys(tarifs)); // tous les exports du module
Les modules ont ouvert la porte à un immense écosystème. Avec npm (le gestionnaire de paquets de Node.js), on installe en une commande des bibliothèques écrites par d'autres : gestion de dates, graphiques, cartes… Des outils de construction comme Vite rassemblent ensuite tous les modules en quelques fichiers optimisés pour la mise en ligne.
Place à la pratique grandeur nature ! Ce chapitre construit un outil complet, celui qu'un client pourrait utiliser sur le site de l'atelier : un calculateur de devis qui se met à jour en direct. Il mobilise la quasi-totalité du cours.
Un bon programme sépare les responsabilités. Le nôtre suit un découpage classique en couches, chacune ignorant les détails des autres :
| Couche | Rôle | Notions du cours |
|---|---|---|
| Données | charger les tarifs | promesses, async / await (ch. 21), objets (ch. 13) |
| Modèle | la classe Devis : lignes, total, validité | classes, champ privé, getters (ch. 16), tableaux (ch. 12), dates (ch. 22) |
| Calcul | transformer les choix du client en devis | fonctions (ch. 11), conditions (ch. 09), boucles (ch. 10) |
| Affichage | dessiner le récapitulatif | DOM (ch. 18-19), Intl (ch. 07 et 22) |
| Interface | lire le formulaire, réagir, mémoriser | événements, FormData (ch. 20), localStorage (ch. 17) |
Sur un vrai site, chaque couche serait un module
(chapitre 23) : tarifs.js, devis.js,
affichage.js et main.js. Ici, pour que tu puisses
tout lire et tout modifier au même endroit, les couches sont rangées dans
un seul programme, séparées par des commentaires.
Remarque les attributs name : c'est grâce à eux que
FormData retrouvera chaque choix. Les trois cases
« prestation » partagent le même nom, et getAll() renverra la
liste de celles qui sont cochées.
Voici le programme entier. Il démarre tout seul : coche des cases dans le rendu, change le SSD, active les options et regarde le devis se recalculer. Puis passe en mode Modifier pour changer un tarif ou ajouter une prestation.
// =============================================================
// 1. DONNÉES — les tarifs, « chargés depuis un serveur »
// =============================================================
const TARIFS_SERVEUR = {
tauxHoraire: 45,
prestations: {
nettoyage: { libelle: "Nettoyage complet", heures: 1 },
systeme: { libelle: "Installation du système", heures: 1.5 },
sauvegarde: { libelle: "Sauvegarde des données", heures: 0.5 },
},
ssd: {
"ssd-500": { libelle: "SSD 500 Go", prix: 49.9 },
"ssd-1000": { libelle: "SSD 1 To", prix: 74.9 },
"ssd-2000": { libelle: "SSD 2 To", prix: 129 },
},
heuresMontageSsd: 0.5,
prixKm: 0.5,
remiseFidelite: 0.1,
prixDevis: 20,
validiteJours: 30,
};
// On simule l'attente du réseau (sur un vrai site : fetch("tarifs.json"))
function chargerTarifs() {
return new Promise(resolve => setTimeout(() => resolve(structuredClone(TARIFS_SERVEUR)), 600));
}
// =============================================================
// 2. MODÈLE — la classe Devis
// =============================================================
class Devis {
#lignes = [];
constructor(validiteJours) {
this.date = new Date();
this.validiteJours = validiteJours;
}
ajouter(libelle, montant) {
this.#lignes.push({ libelle, montant: arrondir(montant) });
return this; // permet d'enchaîner les appels
}
get lignes() {
return [...this.#lignes]; // une copie : personne ne modifie l'original
}
get sousTotal() {
return this.#lignes.filter(l => l.montant > 0).reduce((s, l) => s + l.montant, 0);
}
get total() {
return Math.max(0, arrondir(this.#lignes.reduce((s, l) => s + l.montant, 0)));
}
get validite() {
const fin = new Date(this.date);
fin.setDate(fin.getDate() + this.validiteJours);
return fin;
}
}
const arrondir = (montant) => Math.round(montant * 100) / 100;
// =============================================================
// 3. CALCUL — des choix du client au devis
// =============================================================
function construireDevis(choix, tarifs) {
const devis = new Devis(tarifs.validiteJours);
const taux = tarifs.tauxHoraire;
for (const code of choix.prestations) {
const { libelle, heures } = tarifs.prestations[code];
devis.ajouter(`${libelle} (${formaterHeures(heures)})`, heures * taux);
}
if (choix.ssd) {
const piece = tarifs.ssd[choix.ssd];
devis.ajouter(piece.libelle, piece.prix)
.ajouter(`Montage du SSD (${formaterHeures(tarifs.heuresMontageSsd)})`, tarifs.heuresMontageSsd * taux);
}
if (choix.domicile && choix.km > 0) {
devis.ajouter(`Déplacement (${choix.km} km)`, choix.km * tarifs.prixKm);
}
if (choix.fidele && devis.sousTotal > 0) {
devis.ajouter(`Remise fidélité (${tarifs.remiseFidelite * 100} %)`, -devis.sousTotal * tarifs.remiseFidelite);
}
if (choix.devisRegle && devis.sousTotal > 0) {
devis.ajouter("Devis détaillé déjà réglé", -tarifs.prixDevis);
}
return devis;
}
// =============================================================
// 4. AFFICHAGE — dessiner le récapitulatif
// =============================================================
const euros = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" });
const dateLongue = new Intl.DateTimeFormat("fr-FR", { dateStyle: "long" });
function formaterHeures(heures) {
const h = Math.floor(heures);
const min = Math.round((heures - h) * 60);
if (h === 0) return `${min} min`;
return min ? `${h} h ${String(min).padStart(2, "0")}` : `${h} h`;
}
function cellule(balise, texte, classe) {
const element = document.createElement(balise);
element.textContent = texte;
if (classe) element.className = classe;
return element;
}
function afficherDevis(devis, zone) {
const titre = document.createElement("h2");
titre.append("Votre estimation", cellule("span", `n° ${numeroDevis(devis.date)}`, "numero"));
if (devis.lignes.length === 0) {
zone.replaceChildren(titre, cellule("p", "Diagnostic gratuit : cochez une prestation pour obtenir une estimation.", "discret"));
return;
}
const table = document.createElement("table");
const corps = document.createElement("tbody");
for (const { libelle, montant } of devis.lignes) {
const tr = document.createElement("tr");
tr.append(cellule("td", libelle), cellule("td", euros.format(montant), montant < 0 ? "negatif" : ""));
corps.append(tr);
}
const pied = document.createElement("tfoot");
const totalTr = document.createElement("tr");
totalTr.append(cellule("td", "Total estimé"), cellule("td", euros.format(devis.total)));
pied.append(totalTr);
table.append(corps, pied);
zone.replaceChildren(
titre,
table,
cellule("p", `Estimation indicative, valable jusqu'au ${dateLongue.format(devis.validite)}. Seul le devis détaillé engage l'atelier.`, "discret"),
cellule("p", "TVA non applicable, art. 293 B du CGI.", "discret"),
);
}
function numeroDevis(date) {
const deuxChiffres = (n) => String(n).padStart(2, "0");
const jour = `${date.getFullYear()}${deuxChiffres(date.getMonth() + 1)}${deuxChiffres(date.getDate())}`;
return `D-${jour}-${deuxChiffres(date.getHours())}${deuxChiffres(date.getMinutes())}`;
}
function texteRecapitulatif(devis) {
const lignes = devis.lignes.map(l => `${l.libelle.padEnd(36, ".")} ${euros.format(l.montant)}`);
return [
`Estimation AI29 n° ${numeroDevis(devis.date)}`,
...lignes,
`${"TOTAL".padEnd(36, ".")} ${euros.format(devis.total)}`,
`Valable jusqu'au ${dateLongue.format(devis.validite)} — TVA non applicable, art. 293 B du CGI`,
].join("\n");
}
// =============================================================
// 5. INTERFACE — lire, réagir, mémoriser
// =============================================================
const CLE_STOCKAGE = "ai29-cours-js-devis";
const formulaire = document.querySelector("#formulaire-devis");
const zone = document.querySelector("#recapitulatif");
const information = document.querySelector("#information");
function lireChoix() {
const donnees = new FormData(formulaire);
return {
prestations: donnees.getAll("prestation"),
ssd: donnees.get("ssd"),
domicile: donnees.has("domicile"),
// un champ désactivé est ignoré par FormData : on lit donc « km » directement
km: Math.max(0, Math.round(Number(formulaire.elements.km.value) || 0)),
fidele: donnees.has("fidele"),
devisRegle: donnees.has("devisRegle"),
};
}
function restaurerChoix(choix) {
for (const caseACocher of formulaire.elements.prestation) {
caseACocher.checked = choix.prestations.includes(caseACocher.value);
}
formulaire.elements.ssd.value = choix.ssd ?? "";
formulaire.elements.km.value = choix.km;
for (const option of ["domicile", "fidele", "devisRegle"]) {
formulaire.elements[option].checked = choix[option];
}
}
let devisCourant = null;
function recalculer(tarifs) {
const choix = lireChoix();
devisCourant = construireDevis(choix, tarifs);
afficherDevis(devisCourant, zone);
formulaire.elements.km.disabled = !choix.domicile;
try { localStorage.setItem(CLE_STOCKAGE, JSON.stringify(choix)); } catch { /* stockage indisponible */ }
}
// =============================================================
// 6. DÉMARRAGE
// =============================================================
try {
const tarifs = await chargerTarifs();
console.log("Tarifs chargés :", Object.keys(tarifs.prestations).length, "prestations,", Object.keys(tarifs.ssd).length, "SSD");
try {
const memorise = JSON.parse(localStorage.getItem(CLE_STOCKAGE));
if (memorise) {
restaurerChoix(memorise);
console.log("Choix de la dernière visite restaurés.");
}
} catch { /* rien de mémorisé, ou stockage indisponible */ }
recalculer(tarifs);
formulaire.addEventListener("input", () => recalculer(tarifs));
formulaire.addEventListener("reset", () => setTimeout(() => recalculer(tarifs))); // après la remise à zéro
document.querySelector("#copier").addEventListener("click", async () => {
const texte = texteRecapitulatif(devisCourant);
console.log(texte);
try {
await navigator.clipboard.writeText(texte);
information.textContent = "Récapitulatif copié ✔";
} catch {
information.textContent = "Copie impossible : le texte est affiché dans la console.";
}
});
} catch (erreur) {
zone.replaceChildren(cellule("p", `Tarifs indisponibles : ${erreur.message}`, "alerte"));
}
Devis et construireDevis() ne touchent jamais au DOM : on pourrait les réutiliser telles quelles pour générer un PDF ou un e-mail.arrondir), pour éviter le piège des nombres à virgule du chapitre 07.innerHTML : tout est construit avec createElement et textContent, à l'abri des injections.mailto: avec le récapitulatif dans le corps du message (pense à encodeURIComponent) ;chargerTarifs() par un vrai fetch du fichier tarifs-atelier.json, en adaptant la structure des données.
Bravo, tu as parcouru tout le cours ! 🎉 Partir de
console.log("Bienvenue à l'atelier !") pour arriver à un
calculateur de devis complet, asynchrone et interactif, c'est un sacré
chemin. Récapitulons :
Avec les cours HTML et CSS, tu as maintenant le trio complet du développement front-end : structurer, habiller, animer. Pour continuer à progresser, voici les étapes naturelles :
| Sujet | Pourquoi |
|---|---|
| Pratiquer | refaire de petits projets de A à Z (minuteur, quiz, météo avec une API publique, carnet d'adresses) : c'est en codant qu'on retient |
| Git | garder l'historique de son code, revenir en arrière, travailler à plusieurs |
| TypeScript | JavaScript avec des types vérifiés : moins de bugs sur les gros projets (chapitre 05) |
| Un framework | Astro, Vue, Svelte ou React pour construire des sites et des applications plus vite |
| Node.js | utiliser le même langage côté serveur : API, outils, automatisations |
| Les tests | vérifier automatiquement que le code fait ce qu'il doit, à chaque modification |
Merci de ta lecture, et à bientôt à l'atelier. 👋
↑ Retour au sommaire