Quel JavaScript utilisez-vous ?
ES5, ES6+, transpileurs : le dialecte que vous écrivez change ce que vous pouvez refactorer — et les pièges à connaître.
Avant de refactorer une seule ligne, Burchard pose une question qui semble triviale mais qui ne l'est pas : « quel JavaScript utilisez-vous ? ». Car « JavaScript » n'est pas un langage figé. C'est une famille de dialectes, répartis entre des versions de la spécification, des environnements d'exécution, des langages précompilés et des frameworks. Le code que vous pouvez écrire — et donc les refactorings que vous pouvez appliquer — dépend entièrement de ce socle.
Ce chapitre du livre est volontairement panoramique : il dresse la carte du territoire. Notre objectif ici est d'en extraire ce qui compte concrètement pour le refactoring. Car remplacer var par const, une fonction classique par une fonction fléchée (arrow function), ou == par === n'a de sens que si votre environnement cible sait les exécuter. Choisir et figer un dialecte est la première décision de refactoring, avant même d'ouvrir le code.
JavaScript n'est pas un seul langage
Pour savoir où va le langage, Burchard renvoie à la spécification ECMAScript maintenue par le comité TC39. Les propositions de fonctionnalités y traversent cinq étapes (de 0 à 4), le stade 4 marquant l'adoption. C'est la source canonique : les bibliothèques et implémentations peuvent inventer des fonctionnalités, mais celles qui durent finissent dans la spec.
Mais la spec a deux limites pratiques. D'abord, sa documentation est écrite pour ceux qui construisent des navigateurs, pas pour ceux qui écrivent des sites — autant dire qu'elle est surdimensionnée pour le commun des mortels. Ensuite, et c'est le point crucial pour nous : une fonctionnalité finalisée dans la spec n'est pas garantie d'être disponible dans votre environnement cible. La spec décrit ce qui est probable ; votre runtime décide ce qui est possible.
Concrètement, on distingue plusieurs « JavaScript » :
| Axe | Exemples | Ce que ça change |
|---|---|---|
| Version de la spec | ES5 (2009), ES2015/ES6, ES2017… | Quelle syntaxe est légale. |
| Environnement | Navigateur, Node.js | Quelles API existent (DOM vs fs). |
| Précompilation | Babel, TypeScript, JSX, CoffeeScript | Ce que vous écrivez ≠ ce qui s'exécute. |
| Framework | jQuery, React, Angular, Ember | Du vocabulaire et des contraintes en plus. |
Note
Pour savoir ce qui est réellement supporté, Burchard cite deux ressources : caniuse.com pour les navigateurs (JS, HTML, CSS) et les tables de compatibilité ECMAScript de Kangax pour comparer navigateurs et Node.js. Ce sont des outils de décision avant refactoring, pas de la documentation de fond.
Environnements : le même langage, des codes différents
L'arrivée de Node.js a permis d'écrire « le même langage » côté serveur et côté client. Mais Burchard nuance aussitôt : même si le langage est identique, le code écrit de part et d'autre se ressemble peu. Dans le navigateur, le code gravite autour de l'objet window, du DOM et des capacités de l'appareil. Côté serveur, l'enjeu est la gestion des données et le traitement des requêtes.
Cela a une conséquence directe sur le refactoring : un refactoring valide ici peut être absurde là-bas.
// ❌ Avant : du code navigateur, qui suppose le DOM.
function afficherTotal(total) {
document.querySelector("#total").textContent = total + " EUR";
} // ✅ Après : la logique métier extraite, testable partout.
function formaterTotal(total) {
return total + " EUR";
}
// Le branchement au DOM reste isolé, côté navigateur uniquement.
function afficherTotal(total) {
document.querySelector("#total").textContent = formaterTotal(total);
} En séparant le calcul (formaterTotal, une fonction pure (pure function) sans dépendance à l'environnement) du branchement au DOM, on rend le cœur du code exécutable et testable dans Node.js comme dans le navigateur. C'est d'ailleurs le parti pris du livre : l'essentiel du code y est exécutable sans navigateur, avec les paquets de base de Node.
Transpileurs : écrire le futur, exécuter le présent
Voici le point qui débloque le plus de refactorings. Un transpileur (transpiler) comme Babel prend en entrée du code écrit dans une syntaxe moderne — potentiellement non supportée par la cible — et le compile vers une syntaxe plus ancienne, mieux adoptée. La minification (minification) en est le cas le plus simple : compresser le code en préservant son sens. Mais Babel est né pour autre chose : permettre d'utiliser les fonctionnalités futures de JavaScript dès aujourd'hui.
Trois mots à connaître quand une fonctionnalité de la spec manque à votre runtime : shims, polyfills et transpileurs. Les deux premiers ajoutent des fonctionnalités à l'exécution ; le troisième transforme la syntaxe à la compilation.
Pourquoi est-ce décisif pour le refactoring ? Parce qu'avec un transpileur en place, vous pouvez refactorer vers ES6+ même si la cible ne comprend que ES5.
ce que vous écrivez (ES6+) ce qui s'exécute (ES5)
----------------------------- ------------------------
const f = (x) => x * 2; --> var f = function (x) {
return x * 2;
}; // ✅ Après transpilation : vous écrivez du moderne,
// Babel produit l'équivalent ES5 ci-dessus.
const doubler = (x) => x * 2; À retenir
Décidez de votre socle avant de refactorer : version cible, présence ou non d'un transpileur, environnement. Sans transpileur et avec une cible ES5, passer aux fonctions fléchées ou aux classes casse la production. Avec Babel, ces mêmes transformations deviennent sûres. Le dialecte cible définit l'espace des refactorings autorisés.
D'autres langages précompilés visent un tout autre confort : CoffeeScript (né d'un ras-le-bol des accolades et de l'absence de classes), JSX (mélange de JS et de HTML, utilisé par React) ou Sweet.js (macros et nouveaux mots-clés). Le wiki de CoffeeScript recensait, à l'époque du livre, 337 langages qui compilent vers JavaScript — une bonne mesure de la complexité de l'écosystème. Beaucoup, dont JSX et Sweet.js, passent eux-mêmes par Babel.
Les outils suivent le langage avec retard
Point essentiel et souvent oublié : les outils — linters, formateurs, codemods (transformations automatisées de code) — ne supportent pas instantanément les nouvelles fonctionnalités du langage. Ils suivent, mais avec un décalage. La conséquence pratique : certaines transformations restent manuelles, et certaines transformations automatiques peuvent mal gérer une syntaxe trop récente.
D'où la discipline que Burchard répète tout au long du livre : on refactore par petits pas, sous la protection des tests. Qu'une transformation soit faite à la main ou par un codemod, c'est la suite de tests qui garantit qu'elle n'a rien cassé. Un outil n'est jamais une dispense de tests — il est seulement un accélérateur.
Astuce
Avant d'automatiser un refactoring de masse (par exemple var → let/const sur tout un dépôt via un codemod), vérifiez que l'outil comprend votre dialecte, et lancez-le sur un petit périmètre d'abord. Tests au vert avant, tests au vert après, à chaque lot.
Les pièges du langage qui motivent les refactorings
Si JavaScript impose tant de refactorings, c'est aussi parce qu'il contient des fonctionnalités sources d'imprévisibilité. Burchard rappelle d'ailleurs qu'il existe un sous-ensemble plus sûr et plus rapide, activable avec "use strict" — le mode strict (strict mode) — au niveau d'un fichier ou d'une fonction. Les classes l'incluent par défaut. Activer le mode strict est en soi un premier geste d'assainissement. Passons en revue les pièges les plus courants.
Portée et hoisting : var contre let/const
var a une portée de fonction, pas de bloc, et ses déclarations subissent le hoisting (hoisting) : elles sont remontées en haut de la fonction. Résultat, un classique : une boucle qui partage une seule variable entre toutes ses fermetures.
// ❌ Avant : var, portée de fonction, hoisting.
function planifier() {
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // affiche 3, 3, 3
}, 10);
}
} Le piège : il n'existe qu'un seul i, partagé. Quand les callbacks s'exécutent, la boucle est finie et i vaut 3. La correction tient en un mot : let, à portée de bloc, crée un nouveau i à chaque itération.
// ✅ Après : let, portée de bloc, une liaison par tour.
function planifier() {
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // affiche 0, 1, 2
}, 10);
}
} La règle pratique : const par défaut, let si la variable doit être réassignée, var jamais (sauf cible ES5 sans transpileur). const documente l'intention — « cette liaison ne changera pas » — et empêche une classe entière de bugs de réassignation accidentelle.
// ❌ Avant : var partout, intention illisible.
var taux = 0.2;
var total = 0;
var articles = panier.articles;
for (var j = 0; j < articles.length; j++) {
total += articles[j].prix;
}
// ✅ Après : const pour les constantes, let pour l'accumulateur.
const taux = 0.2;
const articles = panier.articles;
let total = 0;
for (const article of articles) {
total += article.prix;
} Égalité et coercition : == contre ===
L'opérateur d'égalité lâche == applique une coercition (coercion) : il convertit les types avant de comparer, avec des résultats déroutants. L'égalité stricte === compare valeur et type, sans surprise.
// ❌ Avant : == coerce, et les résultats déroutent.
0 == ""; // true
0 == "0"; // true
"" == "0"; // false (!)
null == undefined; // true
[] == false; // true Le pire est l'absence de transitivité : 0 == "" et 0 == "0" sont vrais, mais "" == "0" est faux. Ce genre d'incohérence est un nid à bugs.
// ❌ Avant : test ambigu à cause de la coercition.
function estVide(valeur) {
return valeur == null || valeur == "";
}
// ✅ Après : comparaisons strictes, intention explicite.
function estVide(valeur) {
return valeur === null
|| valeur === undefined
|| valeur === "";
} Attention
Le seul usage défendable de == est x == null, qui teste à la fois null et undefined. Partout ailleurs, refactorez == en === (et != en !==). C'est l'un des refactorings les plus rentables : faible risque, gros gain de prévisibilité. Un linter le signale automatiquement.
Les valeurs falsy
Six valeurs sont falsy (falsy) en JavaScript : false, 0, "", null, undefined et NaN. Tout le reste est truthy (truthy) — y compris "0", "false", [] et {}. Cela rend les tests de présence trompeurs.
// ❌ Avant : 0 est une quantité valide, mais il est falsy !
function appliquerQuantite(commande, quantite) {
if (quantite) {
commande.quantite = quantite;
}
}
// appliquerQuantite(c, 0) n'enregistre rien : bug silencieux. // ✅ Après : on teste précisément ce qu'on veut exclure.
function appliquerQuantite(commande, quantite) {
if (quantite !== undefined && quantite !== null) {
commande.quantite = quantite;
}
} La leçon : if (valeur) ne demande pas « la valeur existe-t-elle ? » mais « la valeur est-elle truthy ? ». Quand 0 ou "" sont des données légitimes, soyez explicite sur la condition réelle.
Le comportement de this
this ne désigne pas l'objet où la fonction est définie, mais dépend de la façon dont la fonction est appelée. Une méthode passée en callback perd son contexte.
// ❌ Avant : this perd son contexte dans le callback.
function Compteur() {
this.total = 0;
this.articles = [1, 2, 3];
this.articles.forEach(function (prix) {
this.total += prix; // this n'est plus le Compteur !
});
} Avant ES6, on contournait avec var self = this, ou bind, ou le second argument de forEach. La fonction fléchée apporte la vraie solution : elle ne crée pas son propre this, elle hérite de celui qui l'entoure (this lexical).
// ✅ Après : la fonction fléchée capture le this englobant.
function Compteur() {
this.total = 0;
this.articles = [1, 2, 3];
this.articles.forEach((prix) => {
this.total += prix; // this est bien le Compteur
});
} Piège courant
Les fonctions fléchées ne sont pas un remplacement universel des fonctions classiques. N'ayant pas leur propre this (ni arguments), elles ne conviennent pas comme méthodes d'objet ni comme fonctions appelées avec new. Refactorer function → => est sûr pour les callbacks, mais à vérifier au cas par cas pour les méthodes.
Classes, modules et autres refactorings ES6+
Une fois le socle moderne adopté (directement ou via Babel), tout un éventail de refactorings s'ouvre. La déstructuration (destructuring), les paramètres par défaut (default parameters) et l'opérateur de décomposition (spread) allègent considérablement le code.
// ❌ Avant : ES5, verbeux et fragile.
function creerCercle(options) {
options = options || {};
var x = options.x === undefined ? 0 : options.x;
var y = options.y === undefined ? 0 : options.y;
return { x: x, y: y, rayon: options.rayon };
}
// ✅ Après : déstructuration + valeurs par défaut.
function creerCercle({ x = 0, y = 0, rayon } = {}) {
return { x, y, rayon };
} L'opérateur de décomposition, lui, fusionne ou clone des objets sans mutation, là où ES5 imposait une boucle ou Object.assign.
// ❌ Avant : on mute defaults, ou on recourt à Object.assign.
function avecDefauts(options) {
return Object.assign({ couleur: "noir", taille: "M" }, options);
}
// ✅ Après : décomposition, sans muter les valeurs par défaut.
function avecDefauts(options) {
return { couleur: "noir", taille: "M", ...options };
} Côté organisation du code, le passage d'un gros fichier à des modules (modules) avec export/import est un refactoring structurant : il rend les dépendances explicites et le code réutilisable.
// ❌ Avant : tout dans un fichier, fonctions globales.
function formaterTotal(total) {
return total + " EUR";
}
function appliquerTaxe(total, taux) {
return total * (1 + taux);
} // ✅ Après : un module, dépendances explicites (panier.js).
export function formaterTotal(total) {
return total + " EUR";
}
export function appliquerTaxe(total, taux) {
return total * (1 + taux);
} // Ailleurs : on importe seulement ce dont on a besoin.
import { formaterTotal, appliquerTaxe } from "./panier.js"; Ces transformations — fonctions fléchées, classes, déstructuration, modules — ne sont disponibles que si le dialecte le permet. C'est tout le sens du chapitre : la liste de vos refactorings possibles est dérivée de votre réponse à « quel JavaScript utilisez-vous ? ».
Choisir, puis figer son socle
Burchard ne tranche pas le débat des frameworks — il propose une démarche : suivre le « hype » (choisir le populaire, bien documenté) quand on hésite, puis savoir revenir au minimalisme. Pour le livre lui-même, il pose des contraintes claires : pas de framework, pas d'étape de compilation ni de transpilation, code exécutable sous Node, quelques bibliothèques de test seulement.
L'enseignement pour le refactoring est là : fixez un socle explicite et stable avant de transformer le code. Version de la spec, environnement, présence d'un transpileur, jeu d'outils. Ce socle détermine ce que vous pouvez écrire, donc ce vers quoi vous pouvez refactorer. Le changer en cours de route revient à déplacer la cible pendant que vous tirez.
Enfin, ne confondez jamais refactoring et réécriture (rewrite). Devant un code hérité, la tentation est grande de tout jeter pour « repartir sur le bon framework ». Mais une réécriture est un processus ambitieux — comprendre « coûteux et risqué ». La voie du livre est l'inverse : avancer par petits pas, du mauvais vers le correct, du bon vers le meilleur, tests à l'appui, sans jamais tout casser.
À retenir
- « JavaScript » est pluriel : versions (ES5, ES6+…), environnements (navigateur, Node), précompilation et frameworks définissent chacun un dialecte différent.
- Le dialecte cible détermine vos refactorings :
let/const, fléchées, classes, modules, déstructuration ne sont disponibles que si le runtime — ou un transpileur comme Babel — les accepte. - Décidez du socle d'abord : version, environnement, transpileur et outils, figés avant de toucher au code. Sans cela, une transformation valide peut casser la production.
- Refactorez les pièges du langage :
var→let/const(portée de bloc, fin du hoisting),==→===(fin de la coercition), gare aux valeurs falsy et authisdynamique. - Les outils suivent le langage en retard : linters et codemods accélèrent, mais certaines transformations restent manuelles ; rien ne remplace les tests à chaque petit pas.
- Refactoring ≠ réécriture : avancer du mauvais vers le correct, par petits pas sous protection des tests, plutôt que tout jeter pour un hypothétique « JavaScript parfait ».