Un tour du langage
Un survol des briques essentielles : fichiers-programmes, valeurs, variables, fonctions, comparaisons et organisation du code.
La meilleure façon d'apprendre JavaScript, c'est de commencer à en écrire. Mais pour cela, il faut savoir comment le langage fonctionne. Ce chapitre n'est ni une référence exhaustive de la syntaxe, ni une initiation complète : c'est un survol des grandes zones du langage, pensé pour vous donner un meilleur ressenti et avancer avec plus de confiance.
Ne vous attendez pas à une lecture rapide. Chaque brique présentée ici — valeurs, variables, fonctions, comparaisons, organisation — sera reprise plus en détail dans les chapitres suivants. Prenez votre temps, comparez ce que vous lisez à des programmes existants, et assurez-vous d'être à l'aise avec chaque section avant de passer à la suite.
Chaque fichier est un programme
Presque toute application web que vous utilisez est composée de plusieurs fichiers JavaScript (typiquement avec l'extension .js). On est tenté de penser l'application entière comme un seul programme. Mais JavaScript voit les choses autrement : en JS, chaque fichier autonome est son propre programme distinct.
Pourquoi est-ce important ? Surtout pour la gestion des erreurs. Comme JS traite les fichiers comme des programmes, un fichier peut échouer (à l'analyse, à la compilation ou à l'exécution) sans nécessairement empêcher le fichier suivant d'être traité. Si votre application dépend de cinq fichiers et que l'un échoue, l'application ne fonctionnera au mieux que partiellement. D'où l'importance de soigner chaque fichier et de gérer le plus gracieusement possible les défaillances des autres.
La seule façon dont plusieurs fichiers autonomes agissent comme un seul programme est de partager leur état (et l'accès à leurs fonctionnalités publiques) via la portée globale (global scope) : ils s'y mélangent dans un même espace de noms, et se comportent à l'exécution comme un tout.
Note
Depuis ES6, JS prend aussi en charge un format de module en plus du programme autonome classique. Un module est lui aussi basé sur un fichier : un fichier chargé via import ou via <script type=module> est traité comme un seul module. Beaucoup de projets utilisent par ailleurs des outils de build qui combinent plusieurs fichiers en un seul ; dans ce cas, JS traite ce fichier combiné comme le programme entier.
Quel que soit le mode d'organisation (autonome ou module), pensez chaque fichier comme son propre mini-programme, susceptible de coopérer avec d'autres mini-programmes pour réaliser l'ensemble de votre application.
Les valeurs
L'unité d'information la plus fondamentale d'un programme est la valeur (value). Les valeurs, ce sont les données : c'est ainsi que le programme conserve son état. En JS, elles existent sous deux formes : les primitives (primitive) et les objets (object). On les insère dans le code à l'aide de littéraux (literal).
Les primitives
Une chaîne de caractères (string) est une collection ordonnée de caractères. On peut la délimiter avec des guillemets doubles "..." ou simples '...' : ce choix est purement stylistique. L'important, pour la lisibilité, est d'en choisir un et de s'y tenir.
Une troisième option existe, l'accent grave (backtick), et celle-là n'est pas qu'esthétique : elle change le comportement. C'est la syntaxe des gabarits de chaîne (template literals), qui permettent l'interpolation (interpolation) d'expressions.
var firstName = "Kyle";
console.log("My name is ${ firstName }.");
// My name is ${ firstName }.
console.log(`My name is ${ firstName }.`);
// My name is Kyle. Seule la chaîne délimitée par des backticks résout l'expression ${ .. } en sa valeur courante. Réservez donc le backtick aux chaînes qui contiennent réellement une interpolation : l'utiliser sans interpolation ne fait qu'ajouter du bruit.
Les autres primitives sont les booléens (boolean), true et false, et les nombres (number). Une variante des nombres est le BigInt (bigint), pour les entiers arbitrairement grands. À noter que les indices de tableau sont basés sur 0 : le premier élément est en position 0.
var names = [ "Frank", "Kyle", "Peter", "Susan" ];
console.log(`My name is ${ names[1] }.`);
// My name is Kyle. Restent null et undefined. Malgré quelques différences, tous deux servent surtout à indiquer l'absence d'une valeur. Beaucoup les traitent indifféremment, mais le plus sûr est de n'utiliser que undefined comme unique valeur « vide », même si null est plus court à taper.
La dernière primitive est le symbole (symbol) : une valeur spéciale, cachée et impossible à deviner, utilisée presque exclusivement comme clé spéciale sur des objets, surtout dans le code bas niveau des bibliothèques et frameworks.
hitchhikersGuide[ Symbol("meaning of life") ];
// 42 Tableaux et objets
À côté des primitives, l'autre type de valeur est l'objet. Les tableaux (array) en sont un cas particulier : une liste ordonnée et indexée numériquement, qui peut contenir n'importe quel type de valeur — primitive ou objet, y compris d'autres tableaux et même des fonctions.
var names = [ "Frank", "Kyle", "Peter", "Susan" ];
names.length; // 4
names[0]; // Frank Les objets sont plus généraux : une collection non ordonnée de valeurs, accessibles par un nom de clé (key) ou propriété (property) plutôt que par une position numérique.
var name = {
first: "Kyle",
last: "Simpson",
age: 39,
specialties: [ "JS", "Table Tennis" ]
};
console.log(`My name is ${ name.first }.`); On accède à une propriété par la notation pointée name.first ou par les crochets name["first"].
Déterminer le type avec typeof
L'opérateur typeof indique le type intégré d'une valeur. Pour une primitive, il renvoie son type précis ; pour le reste, il renvoie "object". Mais il réserve quelques surprises.
typeof 42; // "number"
typeof "abc"; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object" -- oups, un bug !
typeof { a: 1 }; // "object"
typeof [1,2,3]; // "object"
typeof function hello(){}; // "function" Attention
typeof null renvoie malheureusement "object" au lieu du "null" attendu : c'est un bug historique du langage, conservé pour compatibilité. Par ailleurs, typeof renvoie bien "function" pour une fonction, mais pas "array" pour un tableau — un tableau est juste un "object".
Convertir une valeur d'un type vers un autre — par exemple chaîne vers nombre — s'appelle en JS la coercition (coercion). Nous y reviendrons plus bas.
Déclarer et utiliser des variables
Une valeur peut apparaître littéralement, ou être stockée dans une variable (variable) : voyez les variables comme de simples conteneurs pour des valeurs. Une variable doit être déclarée pour être utilisée, et chaque forme de déclaration implique un comportement différent.
var name = "Kyle";
var age;
let nom = "Kyle";
let ageBis; La différence la plus visible entre var et let est que let offre un accès plus restreint à la variable : c'est la portée de bloc (block scoping), par opposition à la portée de fonction de var.
var adult = true;
if (adult) {
var name = "Kyle";
let age = 39;
console.log("Shhh, this is a secret!");
}
console.log(name); // Kyle
console.log(age); // Error! Accéder à age hors du if provoque une erreur : age était limité au bloc, tandis que name, déclaré avec var, ne l'était pas. La portée de bloc est très utile pour limiter la portée des déclarations et éviter les chevauchements accidentels de noms. Mais var reste utile : il communique « cette variable sera vue par une portée plus large ». Les deux formes ont leur place.
Note
On entend souvent qu'il faudrait bannir var au profit de let. Kyle Simpson juge ce conseil trop restrictif et au fond peu utile : il suppose que vous seriez incapable d'apprendre et d'utiliser correctement une fonctionnalité. Apprenez toutes les fonctionnalités du langage, et employez chacune là où elle est appropriée.
La subtilité de const
La troisième forme, const, ressemble à let mais ajoute une contrainte : elle doit recevoir une valeur au moment de la déclaration, et ne peut pas être réassignée ensuite.
const myBirthday = true;
let age = 39;
if (myBirthday) {
age = age + 1; // OK !
myBirthday = false; // Error !
} Attention au piège : une variable const n'est pas « immuable », elle ne peut simplement pas être réassignée. C'est la liaison (binding) qui est constante, pas le contenu. Avec une valeur objet, le contenu reste modifiable même si la variable ne peut pas changer de cible.
const actors = [ "Morgan Freeman", "Jennifer Aniston" ];
actors[2] = "Tom Cruise"; // OK :(
actors = []; // Error ! Astuce
Le meilleur usage sémantique de const est de donner un nom parlant à une valeur primitive simple — myBirthday plutôt que true. Si vous réservez const aux primitives, vous évitez toute confusion entre réassignation (interdite) et mutation (autorisée). C'est la façon la plus sûre de l'utiliser.
Au-delà de var / let / const, d'autres formes déclarent des identifiants : le nom d'une fonction et ses paramètres, ou encore la variable d'une clause catch, qui se comporte comme si elle était déclarée avec let (limitée au bloc catch).
Les fonctions
En JS, le mot fonction (function) prend le sens large de procédure : une collection d'instructions, invocable une ou plusieurs fois, qui peut recevoir des entrées et renvoyer une ou plusieurs sorties. La forme historique est la déclaration de fonction (function declaration).
function awesomeFunction(coolThings) {
// ..
return amazingStuff;
} On parle de déclaration car la fonction apparaît comme une instruction à part entière. L'association entre l'identifiant awesomeFunction et la fonction se fait dès la phase de compilation, avant l'exécution.
À l'inverse, une expression de fonction (function expression) est définie puis assignée à une variable. L'association à l'identifiant n'a lieu qu'au moment de l'exécution de cette instruction.
var awesomeFunction = function(coolThings) {
// ..
return amazingStuff;
}; C'est un point essentiel : en JS, les fonctions sont des valeurs. Elles peuvent être assignées, passées en argument et renvoyées. Ce sont des valeurs de première classe (first-class values), et plus précisément un sous-type particulier de l'objet. Tous les langages ne traitent pas les fonctions ainsi, mais c'est indispensable pour supporter le style fonctionnel, comme le fait JS.
Une fonction reçoit des paramètres (qui agissent comme des variables locales) et peut renvoyer une valeur avec return. On ne renvoie qu'une seule valeur ; pour en renvoyer plusieurs, on les emballe dans un objet ou un tableau.
function greeting(myName) {
return `Hello, ${ myName }!`;
}
var msg = greeting("Kyle");
console.log(msg); // Hello, Kyle! Comme les fonctions sont des valeurs, on peut aussi les assigner comme propriétés d'un objet. À cela s'ajoute une forme plus concise, la fonction fléchée (arrow function), souvent utilisée comme valeur passée à une autre fonction.
var whatToSay = {
greeting() {
console.log("Hello!");
}
};
whatToSay.greeting(); // Hello!
// Fonction fléchée, passée comme argument :
var doubles = [1, 2, 3].map(n => n * 2);
console.log(doubles); // [2, 4, 6] Les comparaisons
Prendre des décisions dans un programme suppose de comparer des valeurs. JS offre plusieurs mécanismes pour cela, et ils sont plus subtils qu'il n'y paraît.
Égal… à peu près
Vous avez forcément croisé l'opérateur ===, dit d'égalité stricte (strict equality). « Strict » semble vouloir dire exact et étroit. Pas exactement. La plupart des valeurs se comportent comme l'intuition le suggère :
3 === 3.0; // true
"yes" === "yes"; // true
null === null; // true
42 === "42"; // false
"hello" === "Hello"; // false
true === 1; // false On décrit souvent === comme « vérifie la valeur et le type ». En réalité, toutes les comparaisons JS tiennent compte du type ; la spécificité de === est d'interdire toute coercition, là où d'autres comparaisons l'autorisent.
Mais === a ses propres pièges : il ment dans deux cas particuliers, NaN et -0.
NaN === NaN; // false (NaN n'est égal à rien, pas même à lui-même)
0 === -0; // true (alors que -0 est une valeur distincte !) Pour ces cas, n'utilisez pas === : préférez Number.isNaN(..), qui ne ment pas sur NaN, et Object.is(..), qui ne ment ni sur -0 ni sur NaN. On peut voir Object.is(..) comme un « quadruple-égal » ====, la comparaison vraiment-vraiment-stricte.
La comparaison d'objets se fait par référence
L'histoire se corse avec les objets. On pourrait croire qu'une égalité examine le contenu — après tout, 42 === 42 compare bien les valeurs. Mais pour les objets, ce n'est pas le cas.
[ 1, 2, 3 ] === [ 1, 2, 3 ]; // false
{ a: 42 } === { a: 42 }; // false
(x => x * 2) === (x => x * 2); // false JS ne définit pas === comme une égalité structurelle (structural equality) pour les objets. Il utilise une égalité d'identité (identity equality) : les objets sont détenus par référence (reference), assignés et passés par copie de référence, et comparés par identité de référence.
var x = [ 1, 2, 3 ];
var y = x; // y référence le *même* tableau que x
y === x; // true
y === [ 1, 2, 3 ]; // false
x === [ 1, 2, 3 ]; // false y === x est vrai car les deux variables pointent vers le même tableau. Les comparaisons avec [1,2,3] échouent car il s'agit chaque fois d'un nouveau tableau différent. Le contenu n'entre pas en jeu, seule compte l'identité de la référence. JS ne fournit aucun mécanisme d'égalité structurelle : il faudrait l'implémenter soi-même, ce qui est bien plus délicat qu'on ne l'imagine (comment comparer deux fonctions, closures comprises ?).
Comparaisons coercitives
La coercition consiste à convertir une valeur d'un type vers sa représentation dans un autre type. C'est un pilier du langage, pas une option à éviter. Là où elle rencontre les comparaisons, la confusion s'installe — en particulier avec l'opérateur ==, souvent qualifié d'égalité lâche (loose equality) et accusé de tous les maux.
Or l'idée reçue selon laquelle == comparerait sans tenir compte des types est fausse. == procède comme === ; la seule différence est que, si les types diffèrent, == autorise une coercition d'abord, puis compare. Mieux vaut donc l'appeler « égalité coercitive ».
42 == "42"; // true ("42" est converti en nombre 42)
1 == true; // true (true est converti en nombre 1) Sachant que == préfère les comparaisons numériques et primitives, vous évitez l'essentiel des cas tordus (comme "" == 0 ou 0 == false). Et n'espérez pas tout régler en utilisant uniquement === : les opérateurs relationnels <, >, <=, >= coercent eux aussi quand les types diffèrent.
var arr = [ "1", "10", "100", "1000" ];
for (let i = 0; i < arr.length && arr[i] < 500; i++) {
// s'exécute 3 fois
} Ici, arr[i] < 500 déclenche une coercition : les chaînes deviennent des nombres (1 < 500, 10 < 500, 100 < 500, puis 1000 < 500 qui est faux). Mais quand les deux valeurs sont déjà des chaînes, la comparaison devient alphabétique.
var x = "10";
var y = "9";
x < y; // true, attention ! À retenir
Il n'existe aucun moyen de désactiver la coercition de ces opérateurs relationnels, sinon de ne jamais comparer des types qui diffèrent. La sagesse n'est donc pas de fuir les comparaisons coercitives, mais de les apprivoiser : comprendre leurs rouages plutôt que les éviter aveuglément.
Comment organiser le code en JS
Deux grands patrons structurent données et comportements dans l'écosystème JS : les classes (class) et les modules (module). Ils ne s'excluent pas — beaucoup de programmes utilisent les deux. À certains égards ils diffèrent, mais à d'autres ce sont deux faces d'une même pièce.
Le patron classe
Une classe définit un « type » de structure de données sur mesure, regroupant des données et les comportements qui opèrent dessus. Une classe n'est pas elle-même une valeur concrète : pour en obtenir une, il faut l'instancier avec new, une ou plusieurs fois.
class Page {
constructor(text) {
this.text = text;
}
print() {
console.log(this.text);
}
}
class Notebook {
constructor() {
this.pages = [];
}
addPage(text) {
var page = new Page(text);
this.pages.push(page);
}
print() {
for (let page of this.pages) {
page.print();
}
}
}
var mathNotes = new Notebook();
mathNotes.addPage("Arithmetic: + - * / ...");
mathNotes.print(); Ici, les données (text, pages) sont regroupées avec leurs comportements (addPage, print). Les méthodes ne peuvent être appelées que sur des instances, pas sur les classes elles-mêmes. Le mécanisme de classe permet aussi l'héritage (inheritance) via extends et super(..), et le polymorphisme (polymorphism) — une méthode enfant qui redéfinit (override) une méthode parente du même nom tout en pouvant l'appeler via super.print().
Le patron module
Le patron module poursuit le même but — grouper données et comportements en unités logiques — mais sa syntaxe est tout autre. Un module classique est une fonction extérieure (qui s'exécute au moins une fois) renvoyant une « instance » exposant des fonctions publiques opérant sur des données internes cachées. Comme c'est une fonction, on parle aussi de fonction-fabrique (module factory).
function Publication(title, author, pubDate) {
var publicAPI = {
print() {
console.log(`
Title: ${ title }
By: ${ author }
${ pubDate }
`);
}
};
return publicAPI;
}
var ydkjs = Publication("YDKJS", "Kyle Simpson", "June 2014");
ydkjs.print(); La différence clé avec la classe : celle-ci stocke méthodes et données sur une instance accessible via this., et tout y est public ; le module, lui, accède aux variables par leur nom dans la portée (sans this.) et expose explicitement une API publique — tout ce qui n'est pas retourné reste privé. À noter qu'on appelle la fabrique comme une fonction normale, sans new.
Les modules ES (ESM), introduits en ES6, partagent cet esprit mais diffèrent par l'implémentation. Le contexte d'enveloppe n'est plus une fonction mais le fichier : un fichier, un module. On expose l'API publique avec export, et au lieu d'instancier, on importe l'unique instance — un ESM est de fait un singleton (singleton).
// fichier publication.js
function printDetails(title, author, pubDate) {
console.log(`Title: ${ title } — By: ${ author }`);
}
export function create(title, author, pubDate) {
var publicAPI = {
print() {
printDetails(title, author, pubDate);
}
};
return publicAPI;
}
// fichier main.js
import { create as newPub } from "publication.js";
var ydkjs = newPub("YDKJS", "Kyle Simpson", "June 2014");
ydkjs.print(); Si un module n'a besoin que d'une seule instance, exportez directement ses méthodes publiques. S'il doit supporter plusieurs instances, fournissez une fonction-fabrique de style module classique, comme create(..) ci-dessus — ce que Kyle Simpson recommande de préférer à class une fois que vous êtes déjà en ESM.
À retenir
- Chaque fichier est un programme : avec ou sans modules, pensez chaque fichier comme un mini-programme qui coopère avec les autres, et soignez la gestion des erreurs entre eux.
- Deux familles de valeurs : les primitives (chaîne, nombre, bigint, booléen,
null,undefined, symbole) et les objets (dont les tableaux et les fonctions).typeofrévèle le type, avec ses pièges (null→"object", tableau →"object"). var/let/const:letetconstont une portée de bloc.constrend la liaison constante, pas le contenu : réservez-le aux primitives.- Les fonctions sont des valeurs de première classe : déclaration, expression ou fonction fléchée, on peut les assigner et les passer partout.
===interdit la coercition mais ment surNaNet-0;==coerce quand les types diffèrent (ce n'est pas un défaut à fuir, mais un comportement à comprendre). Les objets se comparent par référence, jamais par contenu.- Classes et modules sont les deux grands patrons d'organisation : la classe instancie avec
newet expose tout viathis; le module expose explicitement une API et garde le reste privé. Les modules ES sont des singletons basés sur le fichier.