Mémo sur les portées

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.

La plupart de ce qui est couvert ici ne fait pas partie d'ECMAScript standard.

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 vars
  • 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
  • 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 :

Pour toute définition d'une variable, il y a une déclaration de cette variable au début de sa portée et une affectation à l'endroit de sa définition (voir l'exception ECMAScript 6 ci-après).

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 instruction var 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 instruction let 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 instructions let se comportent de la même façon pour le plus au nivau, les déclarations let 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 boucle for 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 boucles for 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éclarations var 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")

Étiquettes et contributeurs liés au document

 Contributeurs à cette page : SphinxKnight
 Dernière mise à jour par : SphinxKnight,