const is the new var
Par Christophe Porteneuve • Publié le 16 mai 2020
• 5 min
This page is also available in English.
Pour le treizième article de notre série quotidienne « 19 pépites de JS pur », nous abordons un sujet un peu controversé : les rôles respectifs des trois mots-clés déclaratifs génériques var
, let
et const
(car pour les deux autres, function
et class
, y’a pas débat).
Dans la série…
Extrait de la liste des articles :
- Extraire les emojis d’un texte
- Définir proprement des paramètres nommés optionnels
const
is the newvar
(cet article)- Utiliser des captures nommées
- Object spread vs.
Object.assign
- …au-delà, c’est la surprise ! (mais la liste est déjà calée)…
Avant / après ES2015
Avant ES2015, JavaScript n’avait que deux mots-clés déclaratifs : var
et function
. Par ailleurs, l’unité de portée n’était pas le bloc (les paires d’accolades), contrairement à de nombreux langages, mais la fonction (et à défaut, la portée globale).
Ça surprend, quand tu viens d’autres langages ; et il n’est jamais bon, pour un langage de programmation, d’avoir un comportement inattendu : c’est une source infinie de bugs.
Du coup, les trois nouveaux mots-clés déclaratifs d’ES2015 (let
, const
et class
) ont le bloc courant comme portée, ce qui est plus aligné avec le paradigme majoritaire en programmation. Leurs déclarations n’existent qu’entre la ligne déclarative et la fin du bloc, fut-il implicite (une boucle sans accolades de blocs a tout de même un bloc implicite).
Le hoisting de var
: un antipattern
Par ailleurs, les deux mots-clés historiques sont hoisted : ça signifie que l’interpréteur JavaScript va considérer la déclaration (mais pas l’initialisation éventuelle) comme positionnée en début de portée, indépendamment de son emplacement. Pour être précis, les var
seront hoisted d’abord, et les function
ensuite.
C’est en fait une tentative d’optimisation préemptive de la toute première implémentation du moteur, qui n’a plus rien à faire ici en 2020, ou 2015, ou même 2009. En fait, c’est même piégeux. Regarde un peu ça :
function wtf() {
console.log(foo)
var foo = 42
console.log(foo)
}
wtf() // => logue 'undefined' puis '42'
C’est contre-intuitif au possible. Ce code ne devrait même pas être syntaxiquement valide. À défaut, il devrait lever une ReferenceError
dès la première ligne de la fonction. Et pourtant, non ! À cause du hoisting, le code réellement exécuté est celui-ci :
function wtf() {
var foo
console.log(foo)
foo = 42
console.log(foo)
}
Il n’y a absolument aucun intérêt au hoisting des variables. Aucun. Zéro. Zilch. En fait, le consensus aujourd’hui est que var
n’a pas sa place dans du code JS moderne (ES2015+).
Au minimum, il devrait donc être remplacé par let
, qui a deux avantages :
- Il n’est pas hoisted, donc toute tentative de le référencer avant sa déclaration lèvera une
ReferenceError
, comme on pourrait s’y attendre. - Sa portée est le bloc courant, ce qui correspond là aussi davantage aux attentes.
Mais alors, pourquoi cet article parle-t-il de const
, et pas de let
?
La réaffectation, ce cas rare
Il y a une seule différence entre let
et const
: le second ne permet pas la réaffectation. Une déclaration const
doit être initialisée à la volée, et ne peut plus être réaffectée (avec par exemple =
) par la suite.
Pourquoi préférer const
?
En pratique, l’écrasante majorité des déclarations ne sont pas réaffectées par la suite ; la raison est simple : modifier la sémantique d’un identifiant au fil du code crée la confusion (ce qui réduit la maintenabilité) et augmente les bugs. Il arrive qu’une réaffectation soit justifiée (affectation asynchrone, affinage progressif d’une donnée, etc.), mais c’est un cas très minoritaire.
En revanche, il est tout à fait possible de réaffecter par erreur. Pourquoi ? Le plus souvent, suite à un copier-coller mal mis à jour, ou à une complétion de code maladroite.
function oops() {
let superComplexStrut = 42
let superComplexStuff = bigCompute()
if (someCondition) {
superComplexStrut *= 2 // Oops, je voulais dire "…Stuff"
}
// …
}
En préférant const
par défaut, on est rattrapés par le col immédiatement lorsqu’on tente de réaffecter un identifiant par erreur. Si on a configuré ESLint (voir plus loin), on repère ça immédiatement (quelques secondes après avoir tapé le code, ou lors du commit au plus tard, si tu as mis des hooks adaptés en place). Sinon, on aura une erreur à l’exécution (si le moteur JS prend en charge const
correctement).
function oops() {
const superComplexStrut = 42
const superComplexStuff = bigCompute()
if (someCondition) {
// ESLint + TypeError: Assignment to constant variable.
superComplexStrut *= 2
}
// …
}
On a tendance à garder notamment let
pour la boucle for
numérique, mais le for
numérique devrait être rarissime maintenant qu’on a l’excellente boucle for…of
(spoiler alert : une pépite en parlera en détail). Pas de réaffectation (genre i++
) ? Pas besoin de let
!
// 🔴 BLEH
for (let i = 0, len = arr.length; i < len; i++) {
const item = arr[i]
// …
}
// ✅ Yeah!
for (const item of arr) {
// …
}
Attention aux paramètres
Dans une fonction, les paramètres de la signature ne sont pas déclarés par let
ou const
, donc fais-y gaffe…
const
≠ immuable
Un autre point important : non réaffectable ne veut pas dire immuable. En particulier, si l’identifiant référence un objet, il reste parfaitement possible de modifier l’objet. C’est juste qu’on ne peut pas réaffecter l’identifiant.
const obj = { name: 'John' }
obj.name = 'Jane' // Parfaitement possible
obj = { name: 'Jean-Gandalf' } // Parfaitement impossible
Si tu souhaites « geler » un objet, tu as plusieurs niveaux de verrouillage possibles ; en premier niveau, la bibliothèque standard fournit depuis ES5 Object.preventExtensions()
, Object.seal()
et Object.freeze()
, par ordre croissant de verrouillage. Pour geler récursivement, il faut aller chercher des utilitaires comme deep-freeze-strict, par exemple. C’est imaginable.
(Évidemment, si on suit les principes de programmation fonctionnelle ou d’immutabilité, ce type de besoin a tendance à disparaître.)
Règles ESLint
Notre bien-aimé ESLint propose plusieurs règles sur ces sujets :
- no-var refusera toute utilisation du mot-clé
var
- prefer-const repèrera toute déclaration
let
ouvar
qui n’est jamais réaffectée et demandera une bascule enconst
- no-const-assign détectera les réaffectations sur un identifiant
const
; en effet, pour peu que ton code soit transpilé en ES5, ou exécuté sur un moteur qui ne gère pas correctementconst
(genre IE11), ça passerait à l’exécution : le linter est alors ton seul rempart. Celui-ci fait d’ailleurs partie du jeu prédéfini de réglages (preset) eslint:recommended.
Si tu veux d’autres points de vue…
J’ai dit en intro que le sujet était controversé : certains estiment en effet que const
devrait être réservé à des constantes « absolues », et non à des étapes de traitement dans le corps d’une fonction par exemple. Ils trouvent que const
n’est pas compatible avec la notion de variable, en oubliant qu’au sein de la majorité de nos algorithmes, nos soi-disant variables… ne varient pas.
Néanmoins, il est toujours bon de s’exposer à plusieurs points de vue avant de se former le sien. Le toujours pertinent Dan Abramov a écrit l’année dernière sur ce sujet, je te recommande de jeter un coup d’œil à ce qu’il en dit.
Envie d’en savoir plus ?
Nos formations envoient du gros pâté, en présentiel ou à distance (FOAD), en inter-entreprises ou en intra rien que pour ta boîte ! Qui plus est, pendant la crise du Covid-19, les salarié·e·s peuvent être formé·e·s gratuitement ! Ce serait vraiment trop bête de ne pas en profiter !