const is the new var
Par Delicious Insights • Publié le 16 mai 2020

Cet article est également disponible en anglais.

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 :

  1. Extraire les emojis d’un texte
  2. Définir proprement des paramètres nommés optionnels
  3. const is the new var (cet article)
  4. Utiliser des captures nommées
  5. Object spread vs. Object.assign
  6. …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 ou var qui n’est jamais réaffectée et demandera une bascule en const
  • 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 correctement const (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 !

Découvrez notre cours vidéo : JavaScript : this is it ! 🖥

Tout savoir sur le fonctionnement de this en JavaScript, des règles fondamentales aux ajustements des API, en passant par les fonctions fléchées, le binding et bien plus encore…