La vue d'ensemble : les trois piliers
Synthèse des trois piliers de JavaScript, la posture « demander pourquoi », et comment poursuivre l'apprentissage.
Ce dernier chapitre referme la boucle. Tout ce que nous avons exploré jusqu'ici — la portée, les fermetures, les prototypes, this, les types, la coercition — n'était pas une collection d'anecdotes à mémoriser, mais l'esquisse d'une architecture. Kyle Simpson organise l'ensemble du langage autour de trois piliers, trois fondations sur lesquelles tient tout programme JavaScript. Les nommer clairement, c'est se donner une carte : on cesse de voir JS comme une suite de bizarreries, et on commence à voir un système cohérent qu'on peut apprendre, pour de bon.
Le titre de la série est lui-même un manifeste : « vous ne connaissez pas ENCORE JS » (you don't know JS yet). Le mot important est « encore ». Personne ne maîtrise ce langage d'un coup ; l'apprentissage est continu, en profondeur, et il commence par une posture — refuser le « ça marche, je ne sais pas pourquoi » — et par une carte des territoires qu'il reste à explorer. C'est l'objet de ce chapitre.
Pilier 1 : portée et fermetures
L'organisation des variables en unités de portée — fonctions, blocs — est l'une des caractéristiques les plus fondamentales de tout langage. Simpson en donne une image : les portées sont des seaux, les variables sont des billes qu'on range dedans, et le modèle de portée du langage fixe les règles déterminant quelle bille va dans quel seau.
Les portées s'imbriquent. Pour une expression donnée, seules les variables du niveau courant ou des niveaux supérieurs (extérieurs) sont accessibles ; celles des niveaux inférieurs (intérieurs) sont cachées. C'est le comportement de la portée lexicale (lexical scope) : les frontières des portées sont déterminées au moment où le programme est analysé (compilé). C'est une décision prise à l'écriture (author-time) : l'endroit où vous placez une fonction décide de la structure de portée de cette partie du programme.
Note
On lit souvent que JS ne serait « pas vraiment » lexicalement porté. C'est faux. Deux caractéristiques nourrissent la confusion : la remontée (hoisting), où les déclarations sont traitées comme si elles figuraient en haut de leur portée, et le fait que les variables déclarées avec var ont une portée de fonction même dans un bloc. Ni l'une ni l'autre n'invalide la portée lexicale ; ce sont des particularités du langage à apprendre, pas des exceptions à la règle.
La fermeture (closure) est la conséquence naturelle de la portée lexicale dès lors que les fonctions sont des valeurs de première classe, ce qu'elles sont en JS. Quand une fonction référence des variables d'une portée extérieure, puis qu'on la transporte ailleurs comme une valeur pour l'exécuter dans un autre contexte, elle conserve l'accès à ses variables d'origine. C'est cela, une fermeture.
function compteur() {
let n = 0; // capturée par la fermeture
return function increment() {
n = n + 1;
return n;
};
}
const suivant = compteur(); // compteur() a fini, mais n survit
console.log(suivant()); // 1
console.log(suivant()); // 2 compteur() a terminé son exécution, et pourtant n n'a pas disparu : increment la garde vivante. Partout en programmation, et tout spécialement en JS, la fermeture porte les motifs les plus importants — au premier rang desquels les modules, la façon la plus naturelle d'organiser du code en JavaScript. (Détails dans le livre 2, Scope & Closures.)
Pilier 2 : les prototypes
Le deuxième pilier est le système de prototypes (prototypes). JS est l'un des très rares langages où l'on peut créer des objets directement, explicitement, sans définir d'abord leur structure dans une classe.
Pendant des années, on a implémenté le motif « classe » par-dessus les prototypes — l'« héritage prototypal » — puis, avec le mot-clé class d'ES6, le langage a redoublé son inclination pour la programmation orientée objet. Simpson estime que cette focalisation a masqué la beauté du système de prototypes : la capacité de deux objets à se connecter et à coopérer dynamiquement, pendant l'exécution d'une méthode, en partageant un contexte this.
À retenir
Une idée reçue tenace : « this désigne la fonction, ou l'endroit où elle est définie ». En réalité, this est déterminé au moment de l'appel, par la façon dont la fonction est appelée. C'est précisément ce qui permet à un objet d'emprunter le comportement défini sur un autre, en partageant son contexte. Les classes ne sont qu'un motif bâti sur cette mécanique.
Les classes sont un motif possible par-dessus cette puissance ; un autre, dans une direction très différente, consiste à embrasser les objets en tant qu'objets, à oublier les classes, et à les laisser coopérer via la chaîne de prototypes. C'est la délégation de comportement (behavior delegation), que Simpson juge plus puissante que l'héritage de classe pour organiser comportements et données.
const Classe = {
accueillir() {
console.log("Bienvenue aux étudiants !");
},
};
// coursMaths n'a pas accueillir(), il délègue à Classe
const coursMaths = Object.create(Classe);
coursMaths.accueillir(); // Bienvenue aux étudiants ! Ici, coursMaths ne possède pas de méthode accueillir. L'appel est délégué, via la chaîne de prototypes, à la méthode définie sur Classe. C'est le même câblage que celui qu'Object.create ou class mettent en place sous le capot — mais montré à nu. (Approfondissement dans le livre 3, Objects & Classes.)
Pilier 3 : les types et la coercition
Le troisième pilier est de loin le plus négligé. La grande majorité des développeurs entretiennent de fortes idées fausses sur le fonctionnement des types, et plus encore sur celui des types en JS. Une vague d'intérêt s'est portée vers le « typage statique » via des outils comme TypeScript ou Flow.
Simpson est d'accord pour dire que les développeurs JS devraient mieux connaître les types et la façon dont JS gère les conversions, et que les outils conscients des types peuvent aider — à condition d'avoir d'abord acquis cette connaissance. Mais il refuse la conclusion selon laquelle le mécanisme de types de JS serait « mauvais » et qu'il faudrait le recouvrir avec des solutions extérieures au langage.
Attention
Aucun programme JS ne fera quoi que ce soit d'utile s'il n'exploite pas correctement les types de valeurs et leur conversion — la coercition (coercion). Même si vous adorez TypeScript, vous n'en tirerez pas le meilleur sans connaître intimement la façon dont le langage gère lui-même les types de valeurs. Et surtout : ne sautez pas ce sujet sous prétexte qu'on vous a toujours dit « utilise === et oublie le reste ».
La coercition est partout, y compris là où on ne la voit pas. Toute condition (if, ternaire, while, for) effectue une comparaison implicite vers un booléen. La croire équivalente à == true est un modèle mental inexact :
var x = "hello";
if (x) {
// s'exécute
}
if (x == true) {
// ne s'exécute PAS : true devient 1, puis "hello" devient NaN -> NaN == 1 est false
}
// Le modèle correct : une coercition vers booléen précède
if (Boolean(x) === true) {
// s'exécute
} if (x) ne teste pas x == true ; il convertit x en booléen d'abord. Comme Boolean(..) renvoie toujours un booléen, == et === y sont équivalents. On ne peut pas échapper aux coercitions dans les comparaisons JS : autant les apprendre. (Le livre 4, Types & Grammar, leur est consacré.)
Pourquoi maîtriser les trois piliers
Tant qu'on ne possède pas ces fondations, son socle en JS est « bancal et incomplet, au mieux ». La conséquence pratique est concrète : on passe son temps à se battre contre le langage, à empiler des contournements pour des comportements qu'on ne comprend pas. À l'inverse, quand portée, prototypes et types deviennent limpides, on écrit du code intentionnel — chaque ligne fait ce qu'on a voulu, parce qu'on sait pourquoi elle le fait.
// Récapitulatif des trois piliers en quelques lignes
function fabriquerSalutation(prefixe) {
// Pilier 1 : prefixe est capturé par fermeture
return function saluer(nom) {
return prefixe + ", " + nom + " !";
};
}
const bonjour = fabriquerSalutation("Bonjour");
console.log(bonjour("Kyle")); // Bonjour, Kyle !
const personne = { nom: "Kyle" };
// Pilier 2 : toString est délégué via la chaîne de prototypes
console.log(personne.toString()); // [object Object]
// Pilier 3 : coercition implicite du nombre vers string lors de la concaténation
console.log("Salut " + personne.nom + ", tu as " + 30 + " ans"); // Salut Kyle, tu as 30 ans Demander pourquoi
Au cœur de toute la série se trouve une posture : demander pourquoi (asking why). Refuser le « ça marche, je ne sais pas pourquoi » et creuser jusqu'à la racine. Les faits sur JS ne se débattent pas : soit la spécification dit quelque chose, soit elle ne le dit pas. Si quelqu'un — un collègue, un examinateur d'entretien — prétend que vous avez tort sur un fait, proposez de vérifier dans la spécification sur-le-champ.
C'est ce qui distingue l'apprentissage profond de la simple recette. Comprendre le pourquoi d'un comportement, c'est pouvoir le prévoir dans un contexte inédit, au lieu de réciter un cas particulier. Simpson partage ses opinions, mais ne les présente jamais comme des faits ; il vous invite à vous approprier les vôtres et à savoir les défendre — « n'ânonnez pas ce que je dis ».
Astuce
« Suivre le grain » du langage, c'est écrire un JS reconnaissable comme du JS, avec ses motifs et ses idiomes propres, plutôt que de plaquer du Java ou du Python. Et quand vous faites évoluer un code existant vers de meilleures pratiques, allez-y petit à petit, un sujet à la fois, en laissant les comparaisons avant/après faire l'essentiel du travail de conviction.
Tant de formes de fonctions
L'annexe A insiste sur un point qui surprend : il existe en JS un nombre étonnant de formes de fonctions, et il faut savoir les reconnaître. Commençons par la distinction la plus piégeuse, l'expression de fonction anonyme (anonymous function expression) face à l'expression nommée (named function expression).
// Expression anonyme : pas de nom entre `function` et `(`
var awesome = function (choses) {
return choses;
};
awesome.name; // "awesome" — un nom INFÉRÉ depuis l'affectation
// Expression nommée : l'identifiant `someName` est explicite
var awesome2 = function someName(choses) {
return choses;
};
awesome2.name; // "someName" Depuis ES6, JS pratique une « inférence de nom » : une fonction anonyme affectée avec = reçoit un nom dérivé de la variable. Mais ce nom inféré n'est qu'une chaîne de métadonnées, pas un identifiant utilisable depuis l'intérieur de la fonction — pour la récursion ou le retrait d'un écouteur d'événement, par exemple. De plus, l'inférence ne se produit que dans des cas limités (affectation), pas quand on passe la fonction comme argument : dans ce cas, name est vide.
Simpson défend une règle : nommez vos fonctions. Si une fonction existe, elle a un but ; et si elle a un but, elle a un nom naturel qui le décrit. Même un corps trivial comme x * 2 oblige le lecteur à le parcourir mentalement pour deviner « double » ; lui donner ce nom une fois épargne ce travail à chaque lecture future. C'est un gain pour le débogage (les traces d'erreur portent le nom), la lisibilité et la récursion.
Vient ensuite l'inventaire — déclarations, puis expressions, puis méthodes :
// Déclarations
function classique() {}
function* generatrice() {}
async function asynchrone() {}
// Expressions, dont l'IIFE (exécutée immédiatement)
(function nomDIIFE() {})();
(async function () {})();
// Fonctions fléchées : syntaxiquement ANONYMES
var f;
f = () => 42;
f = (x) => x * 2;
f = (x, y) => x * y;
f = (x) => ({ x: x * 2 }); // renvoyer un objet : parenthèses
// Méthodes (dans un objet ou une classe)
var obj = {
coolMethode() {}, // virgules dans un objet
oldSchool: function () {},
}; Piège courant
Les fonctions fléchées (arrow functions) sont syntaxiquement anonymes : la syntaxe n'offre aucun moyen de leur donner un nom direct. Elles ont un but précis — traiter this de façon lexicale — mais cela ne signifie pas qu'il faille les employer pour tout. Comme Simpson n'apprécie pas les fonctions anonymes, il n'est pas fan de la flèche par défaut. Choisissez l'outil le plus adapté à chaque tâche, pas le plus court.
Il n'y a pas de raccourci : il faut bâtir une familiarité avec toutes ces formes pour les reconnaître dans le code existant et les employer à bon escient. Étudiez-les de près, et pratiquez.
Comment poursuivre
La série You Don't Know JS Yet déplie chaque pilier dans son propre livre. L'ordre suggéré :
- Get Started — la fondation (ce livre).
- Scope & Closures — pilier 1 : portée lexicale, fermetures, modules.
- Objects & Classes — pilier 2 :
this, prototypes, délégation, classes. - Types & Grammar — pilier 3 : types, coercition, grammaire.
- Sync & Async — le contrôle de flux dans le temps.
- ES.Next & Beyond — l'avenir proche du langage.
Les livres 2, 3 et 4 se lisent dans l'ordre qui vous attire le plus — mais n'en sautez aucun, pas même Types & Grammar, malgré la tentation. Le livre 5 est crucial mais peut être différé si l'asynchrone vous intimide : plus vous aurez écrit (et galéré avec) du JS, plus vous l'apprécierez.
Astuce
Avant de courir au livre suivant, prenez votre temps. Relisez ce livre, comparez son contenu au code de vos projets actuels. Et surtout : pratiquez. « Il n'y a pas de meilleure façon d'apprendre le code que d'en écrire. »
À retenir
- Trois piliers structurent JS : (1) portée et fermetures, fondées sur la portée lexicale et base des modules ; (2) les prototypes, avec
thiset la délégation ; (3) les types et la coercition. - Les maîtriser change la pratique : on cesse de se battre contre le langage et on écrit du code intentionnel ; sans ce socle, la fondation reste bancale.
- Corrigez les idées reçues : JS est lexicalement porté,
thisdépend de l'appel (pas de la définition), etif (x)coerce vers booléen — ce n'est pasx == true. - Demandez toujours pourquoi : refusez le « ça marche sans savoir pourquoi », creusez jusqu'à la spécification, et appropriez-vous vos opinions au lieu de les réciter.
- Nommez vos fonctions et connaissez leurs formes (déclaration, expression nommée/anonyme, fléchée, méthode, IIFE) : un nom explicite sert le débogage, la lisibilité et la récursion.
- L'apprentissage est continu : « vous ne connaissez pas ENCORE JS » — poursuivez avec la suite de la série, en suivant le grain du langage et en pratiquant délibérément.