Appeler une fonction dix lignes avant de l'avoir écrite fonctionne. Lire une variable avant sa déclaration donne undefined dans un cas, et une erreur dans l'autre. Ces trois comportements sortent du même mécanisme.
On le résume souvent par une image trompeuse, celle de déclarations qui remonteraient en haut du fichier. Rien ne bouge réellement : c'est le moteur qui prend connaissance des noms avant d'exécuter quoi que ce soit.
Définition
Le nom existe, mais il reste inaccessible jusqu'à sa ligne de déclaration. Cet intervalle porte un nom, la zone morte temporelle. Plutôt que de rendre une valeur trompeuse, le langage refuse la lecture et l'erreur pointe la ligne fautive.
Les vingt-quatre états de cette démo, six déclarations par quatre lignes de lecture, ont été exécutés tels quels sous Node : les sorties et les messages d'erreur affichés ici sont les siens, en anglais comme il les produit. Les lignes // ... n'ont aucun effet, elles marquent seulement les emplacements où la lecture peut se poser.
Changez le mot-clé et l'endroit de la lecture. Le même code donne une valeur ou une erreur, et c'est le mot-clé qui décide.
Le hoisting, ou remontée, désigne le fait que les déclarations d'un bloc sont enregistrées par le moteur avant l'exécution de ce bloc. Le nom existe donc dès la première ligne. Ce qui change d'un mot-clé à l'autre, c'est la valeur qu'il porte à ce moment.
| Déclaration | Avant la ligne de déclaration |
|---|---|
function nom() {} | Utilisable, corps compris |
var nom | Existe, vaut undefined |
let nom et const nom | Existe, mais toute lecture lève une erreur |
class Nom {} | Existe, mais toute lecture lève une erreur |
const nom = () => {} | Suit la règle de const, pas celle des fonctions |
Ce tableau à deux colonnes suffit à expliquer presque toutes les surprises du sujet.
direBonjour(); // fonctionne
function direBonjour() {
console.log("bonjour");
}
console.log(compteur); // undefined, et non une erreur
var compteur = 3;
console.log(compteur); // 3La zone morte temporelle
Entre le début du bloc et la ligne de déclaration, une variable let ou const existe sans être lisible. Cet intervalle porte un nom, la zone morte temporelle, et toute lecture qui y tombe échoue.
try {
console.log(total);
let total = 3;
} catch (erreur) {
console.log(erreur.constructor.name); // ReferenceError
}C'est un choix délibéré : plutôt qu'une valeur trompeuse, le langage refuse la lecture. Le message d'erreur pointe directement la ligne fautive, là où var laissait passer un undefined qui ne se manifestait que bien plus loin.
Ce qu'il faut en faire au quotidien
- Déclarez avant d'utiliser. La règle rend le hoisting invisible, ce qui est le meilleur état possible.
- Comptez sur la remontée des fonctions. Ranger les fonctions utilitaires en bas d'un fichier reste parfaitement valable.
- Ne comptez pas dessus pour une Fonction fléchée. Stockée dans une constante, elle suit les règles de
const. - Oubliez
var. Sa remontée silencieuse enundefinedest la seule vraie source d'ennuis ici.
Questions fréquentes
Le code est-il vraiment déplacé dans le fichier ?
Non, et c'est le principal malentendu. Rien n'est réécrit ni déplacé. Le moteur parcourt le bloc une première fois pour enregistrer les noms déclarés, puis exécute les lignes dans l'ordre où elles sont écrites. L'image de la remontée est une métaphore pédagogique, pas une description du fonctionnement.
Pourquoi une expression de fonction ne remonte-t-elle pas ?
Parce que ce qui est déclaré, c'est la variable, pas la fonction. Avec const traiter = function () {}, seul le nom traiter est enregistré, et sa valeur n'est affectée qu'à l'exécution de cette ligne. Appeler traiter() plus haut échoue donc comme n'importe quelle lecture anticipée.
Les paramètres d'une fonction sont-ils concernés ?
Ils sont enregistrés avant le corps de la fonction, ce qui explique qu'un Paramètre par défaut puisse s'appuyer sur un paramètre déclaré avant lui. En revanche une déclaration let du corps ne peut pas réutiliser le nom d'un paramètre : le moteur signale un conflit dès l'analyse du fichier.