JavaScript, avec les extensions Mozilla, possède des instructions var
dont la portée est celle de la fonction et des instructions let
dont la portée est celle du bloc. Ces instructions, combinées à l'élévation (hoisting en anglais) et au comportement dynamique de JavaScript font que les notions de portées réservent quelques surprises.
var
- portée de la fonction
- remonté en haut de sa fonction
- les re-déclarations du même nom dans la même portée n'ont aucun effet
const
- portée de la fonction
- remonté en haut de sa fonction
- les re-déclarations du même nom dans la même portée sont refusées
let
- portée d'un bloc
- remonté en haut de son bloc (pas pour ECMAScript 6 !)
- les re-déclarations sont illégales
- le comportement est exactement le même que l'instruction
var
pour le plus haut niveau d'une fonction (c'est-à-dire qu'on peut redéclarer un nom dans une fonction, au plus haut niveau mais nul part ailleurs)
function
Il existe trois formes différentes, chacune avec un comportement différent :
- déclaration : une instruction présente au plus haut niveau de la fonction
- se comporte comme un
var
qui initialise une variable et la lie à cette fonction - l'initialisation « remonte » au plus haut de la fonction parente, au-dessus des
var
s
- se comporte comme un
- instruction : une instruction au sein d'un bloc fils
- se comporte comme un
var
qui initialise une variable et la lie à cette fonction - ne remonte pas au plus haut de la fonction parente
- se comporte comme un
- expression : à l'intérieur d'une expression
- liée uniquement à l'expression
Élévation (Hoisting)
L'élévation est peut-être l'élément le plus surprenant et celui qui peut causer le plus de soucis en termes de portée. La principale chose à garder en mémoire est la suivante :
Ceci est également valable pour l'ombrage de variables et le calcul de variables « upvars » (NdT : à affiner).
L'élévation de variables peut également entraîner des « collisions » entre des instructions var
et let
. Les exemples suivants contiendront des erreurs.
- une instruction
let
ne peut pas s'élever au dessus d'une instructionvar
pour la déclaration d'une variable du même nom
function f() { { var x; let x; // erreur, l'élévation croise var x } }
- Une instruction
var
ne peut pas s'élever au dessus d'une instructionlet
pour une variable du même nom
function f() { { let x; { var x; // erreur, l'élévation croise let x } } }
Étant donné que les instructions let
se comportent comme des instructions var
au plus haut niveau de la fonction, le code suivant fonctionne :
function f() { let x; { var x; // OK, on ne fait que redéclarer une variable, ça n'a pas d'effet } }
Cela peut avoir des effets surprenant sur l'utilisation de blocs catch
.
function f() { try { throw "e"; } catch(x) { var x; x = "catch"; // affectation au x local du bloc } print(x); // undefined }
Avec ECMAScript 6, let
n'élève pas la variable en haut du bloc. Si on fait référence à une variable d'un bloc avant que la déclaration let
ait été rencontrée, cela provoquera une erreur ReferenceError
, car la variable est dans une « zone morte » entre le début du bloc et jusqu'au moment de la déclaration.
function f() { console.log(x); // ReferenceError let x = 2; }
Paramètres
- Plusieurs paramètres d'une fonction peuvent porter le même nom. Seul le dernier est lié au contenu.
function f(x, x) { print(x); } f("toto", "truc"); // "truc"
- Les noms des paramètres peuvent ombrager le nom de la fonction à l'intérieur de sa portée, par exemple :
function f(f) { print(f); } f("toto"); // "toto"
- Les déclarations
var
, quant à elle, n'ombragent pas les noms des paramètres. Étant donné que les instructionslet
se comportent de la même façon pour le plus au nivau, les déclarationslet
n'ombragent pas non plus les noms des paramètres ! Le code suivant affichera "toto" car la déclaration n'a aucun effet, il existe déjà un paramètre nomméx
.
function f(x, y) { var x; arguments[0] = "toto"; print(x); // "toto" }
with
capture les affectations mais pas les déclarations var
Grâce à l'élévation, les injections d'objets dans la portée via with
peuvent entraîner des confusions entre les affectations à une variable et les affectations des propriétés des objets insérés. Les définitions de variables sont à décomposer en deux parties : la déclaration et l'affectation. Utiliser var
à l'intérieur d'un with
peut parfois avoir des résultats inattendus... LEs deux exemples qui suivent sont équivalents :
function f() { var o = {x: "toto"}; with (o) { var x = "truc"; } print(o.x); // "truc" }
function f() { var x; var o = {x: "toto"}; with (o) { x = "truc"; } print(o.x); // "truc" }
On notera également que les instructions let
se comportent comme attendues car les déclarations ne sont pas élevées en dehors with
. Elles masqueront (shadowing) les propriétés qui ont le même nom.
eval
peut capturer les affectations mais pas les déclarations var
Les instructions var sont élevées de façon classique, ainsi les instructions eval
peuvent capturer les affectations, de façon semblable à with
:
function f() { { let x = "interne"; eval("var x = 'externe'"); print(x); // "externe" } }
for
: définition du pas d'itération
- Les instructions
var
contenues dans les trois expressions permettant de définir le comportement d'un bouclefor
sont élevées en haut de la fonction. Les deux exemples sont équivalents :
function f() { for (var i = 0; i < c; i++) { ... } }
function f() { var i; for (i = 0; i < c; i++) { ... } }
C'est pourquoi, il n'est pas sûr d'imbriquer des déclarations var utilisant le même nom de variable dans des boucles for
imbriquées, même si le but est de masquer la variable de la boucle parente.
- L'utilisation d'instructions
let
dans les débuts de bouclesfor
crée, implicitement, un bloc autour de la condition, de l'incrément de la boucle et du corps de la boucle. Les deux exemples qui suivent sont équivalents et illustrent ce concept :
function f() { for (let i = 0; i < c; i++) { ... } }
function f() { { let i; for (i = 0; i < c; i++) { ... } } }
Il n'y a pas de nouvelle instruction let
pour chaque itération. On a un seul let
autour de la boucle entière. Ce comportement pourrait être revu à l'avenir bug 449811.
Les variables de catch
ont une portée de bloc
Les variables interceptées dans les blocs catch
ont une portée correspondante au bloc, comme pour les instructions let
.
function f() { try { throw "toto"; } catch (e) { } // e n'est pas défini ici }
Instructions et expressions let
- Les instructions
let
créent des liaisons avec les blocs qui les accompagnent :
function f() { let (x) { x = "toto"; print(x); // "toto" } // x n'est pas défini ici let (x = "truc") { print(x); // "truc" } // x n'est pas défini ici }
- Les expressions
let
créent des liaisons avec l'expression qui les accompagne :
function f() { (1 + (let (i = 1) i)); // 2 ((let (i = 1) i) + i); // erreur, le dernier i n'est pas défini }
Étrangeté des fonctions
- Les instructions
function
ne sont pas élevées quand elles sont déclarées au sein d'un bloc fils :
function f() { { g(); // erreur : g n'est pas défini function g() { ... } } }
- « portée dynamique » : la portée d'une fonction parente, contenant une fonction fille, peut être modifiée lors de l'exécution :
function g() { print("globale"); } function f(cond) { if (cond) { function g() { print("interne"); } } g(); // "interne" quand cond est vérifiée, "globale" quand !cond }
- Les fonctions nommées dans des expressions de fonction font partie de la portée de l'expression. Leurs noms ne sont liés qu'à l'intérieur de l'expression de définition. Ils n'influencent pas la portée existante.
function f() { (function g() { print("g"); })(); g(); // erreur, g n'est pas défini }
- Les initialisations de fonctions ont lieux en haut de la fonction parente (avant les
var
). Étant donné que les déclarationsvar
dont les noms sont déjà pris par les fonctions n'ont aucun effet, on peut obtenir des résultats déconcertants :
function f() { function g() { print("toto"); } var g; g(); // "toto" }
function f() { var g = 0; function g() { print("toto"); } g(); // erreur g n'est pas une fonction car l'initialisation de la fonction est surchargée par l'affectation à 0 (qui a lieu après) }
- Les fonctions ne sont pas élevées du tout si elles sont contenues dans un bloc. En revanche, elles peuvent modifier la portée existante
function f() { var g = 0; if (cond) { function g() { print("toto"); } } g(); // affiche "toto" quand cond est vérifiée, une erreur sinon }
Prédicats de sélecteur E4X
Les prédicats de sélecteur E4X ajoutent un élément XML à la chaîne de portées afin d'évaluer une expression filtre
list = <><item><name>toto</name></item><item><name>truc</name></item><item><name>bidule</name></item></>; subList = list.(String(name) === "bar")