Blog Arolla

JavaScript et le hoisting

Dans cet article, je souhaiterais évoquer avec vous le ‘hoisting’. Je vais tenter d’expliquer comment cela se passe dans les coulisses de JavaScript lorsqu’on déclare ou définit une fonction ou une variable. Rien n’est laissé au hasard, et finalement, ce qui pouvait sembler être une bizarrerie liée au langage s’explique très facilement en quelques lignes de code.

Définition

D’après WordReference, le verbe anglais to hoist signifie : remonter, élever, hisser. Eh bien, c’est ce qui se passe avec JavaScript. Avant d’être exécuté, l’interpréteur va remonter toutes les déclarations de variables et de fonctions tout en haut de leur contexte d’exécution.

Il existe deux types de contexte :

  • La fonction dans laquelle la déclaration a été faite
  • Le contexte d’exécution globale si la déclaration a été faite en dehors de toute fonction

Les exemples ci-dessous vous aideront à mieux comprendre ce mécanisme de remontée.

Oh hisse!

Les variables

D’après vous, que retourne le code suivant?

console.log(a);
var a = 2;

Il faut bien différencier déclaration et affectation. Pourquoi? Chaque déclaration de variable est placée tout en haut de sa portée par l’interpréteur JavaScript. Dans l’exemple ci-dessus, la portée se trouve dans le contexte d’exécution global, car la variable n’est déclarée dans aucune fonction. Ainsi, l’interpréteur va remonter la déclaration de la variable avant le reste. Cela revient à écrire ceci :

var a;
console.log(a);
a = 2;

Ainsi, on comprend donc mieux maintenant pourquoi le résultat affiché est undefined.

On aurait aussi très bien pu écrire le code suivant pour avoir le résultat attendu :

a = 1;
console.log(a);
var a;

Ici, on pourrait croire que a est une variable globale. Or, elle est déclarée à la fin. Comme l’interpréteur remonte la déclaration de la variable avant son affectation, cela revient à écrire :

var a;
a = 1;
console.log(a);

Prenez donc garde à bien déclarer vos variables au tout début de votre code, cela vous évitera bien des désagréments!

Les fonctions

Déclaration de fonction

Soit maintenant le code suivant :

foo();
function foo() {
console.log('foo');
}

Bien que la fonction foo soit déclarée après son appel, le résultat affiché est bien celui attendu, à savoir ‘foo’. Dans ce cas, puisqu’il s’agit d’une déclaration, la fonction va être déplacée par l’interpréteur au-dessus de son appel. On aura donc :

function foo() {
console.log('foo');
}
foo();

Expression de fonction

Reprenons le précédent code et changeons-le pour en faire une expression de fonction en lieu et place d’une déclaration :

foo();
var foo = function() {
console.log('foo');
}

On obtient un joli “Uncaught TypeError : foo is not a function”. En tant qu’expression de fonction, l’interpréteur JavaScript ne va ni plus ni moins que remonter la déclaration de la variable foo et laisser l’affectation à son emplacement initial. Cela revient donc à écrire le code suivant :

var foo;
foo();
foo = function() {
console.log('foo');
}

Ici encore, le résultat est finalement tout à fait logique. Il faut donc bien faire la distinction entre expression et déclaration de fonction, car l’interpréteur va les traiter de manière différente l’une de l’autre.

Cohabitation

Mettons un peu de folie dans ce code et ajoutons-y des variables :

var f = 'foo';
foo();
function foo() {
console.log(f);
var f = 'bar';
}

Étrange cet undefined? Pas tant que ça! Ici, nous avons deux variables du même nom, mais pas au même endroit. Si on applique le raisonnement, l’interpréteur organise le code comme suit :

function foo() {
var f;
console.log(f);
f = 'bar';
}
var f;
f = 'foo';
foo();

On voit bien que le contexte d’exécution des deux variables diffère. L’une est dans le contexte global, tandis que l’autre se trouve dans celui de la fonction. Elles sont remontées, mais pas au même niveau. Voilà pourquoi on obtient undefined à l’exécution de foo.

Un mot sur les variables globales…

Que se passe-t-il si on supprime la variable f du contexte global?

foo();
foo();
function foo() {
try {
console.log(f);
}
catch (ex) {
console.log("f n'existe pas");
}
f = 'bar';
}

L’interpréteur va organiser le code comme ceci :

function foo() {
try {
console.log(f);
}
catch (ex) {
console.log("f n'existe pas");
}
f = 'bar'; // variable globale
}
foo();
foo();

Comme f n’existe pas au premier appel de foo, cela va afficher ‘f n’existe pas’. Or, on affecte juste après une valeur à f qui devient une variable globale. Car c’est la règle : toute variable non déclarée devient une variable globale. On obtient donc :

f n’existe pas
bar

… et un petit sur le mode strict

Rajoutez donc la ligne 'use strict'; tout en haut du code ci-dessus. Vous obtenez une belle erreur vous indiquant que f n’est pas définie. Cela est dû au fait que vous êtes passé en mode strict, et que ce mode est, par définition, bien moins permissif que celui par défaut. Ainsi, en passant en mode strict, impossible de créer une variable globale. Ce mode vous prévient entre autre d’erreurs liées à des oublis de déclaration. Pour plus de détails, je vous laisse le soin d’aller regarder sur le site de Mozilla.

Conclusion

Le sujet est vaste, mais j’espère cependant que cet article vous aura permis de comprendre un peu mieux comment cela se passait quand on manipule des variables et des fonctions. C’est pour cela qu’on entend assez souvent dire qu’il faut déclarer toutes vos variables au début de votre programme. Utilisez quand c’est possible le mode strict afin de réduire les risques de bugs liés à une mauvaise utilisation des variables. Cela vous évitera des séances de débogage interminables sur des problèmes de références à undefined ou autre joyeusetés dont JavaScript est si friand!

Happy hoisting!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *