global scope / visibilité globale

Une variable définie globalement ou dans l'objet window a une visibilité globale dans toutes les fonctions. Les variables déclarées avec "var" dans les blocs <script> ou dans les fichiers .js sont définies globalement si elle ne sont pas dans des fonctions.

var a = 1;
function f() {
  console.log(window.a, window['a'], a); /* affiche 1, 1, 1 */
  function g() {
    console.log(window.a, window['a'], a); /* affiche 1, 1, 1 */
  }
  g();
}
f();
[voir exemple 1]

Ce code définit une variable "a" avec la valeur 1. On pourrait aussi écrire window['a'] = 1 ou window.a = 1

Les variables utilisées sans l'instruction "var" sont globales, même dans les fonctions.

function f() {
  a = 1;
  console.log(window.a, window['a'], a); /* affiche 1, 1, 1 */
  function g() {
    console.log(window.a, window['a'], a); /* affiche 1, 1, 1 */
  }
  g();
}
f();
console.log(window.a, window['a'], a); /* affiche 1, 1, 1 */
[voir exemple 2]

L'instruction var est lue avant d'exécuter le code séquentiellement. Ce qui veut dire que si on utilise la syntaxe a = 1; var a = 1; dans une fonction, la variable n'est pas globale.

function f() {
  a = 1;
  console.log(window.a, window['a'], a); /* affiche undefined, undefined, 1 */
  var a;
  function g() {
    console.log(window.a, window['a'], a); /* affiche undefined, undefined, 1 */
  }
  g();
}
f();
console.log(window.a, window['a']); /* affiche undefined, undefined */
/*  on ne peut pas utiliser a car la variable n'est pas définie et on a un ReferenceError */
[voir exemple 3]

Lorsque la variable "this" n'est pas associée à un objet dans une fonction, this représente l'objet global (window dans une page Web).

function f() {
 this.a = 1;
 console.log(this.a, window.a, a); /* affiche 1, 1, 1 */
}
f();
console.log(window.a, a); /* affiche 1, 1 */
[voir exemple 4]

Dans l'exemple précédent, si on utilise new f() alors window.a sera "undefined" et a sera une variable non définie (erreur à l'accès).

On peut utiliser le fait que this est l'objet global dans ce genre de fonction (non assigné à un objet) pour exporter des variables lorsque l'objet window est redéfini.

function f() {
  var window = 'window'; // redéfinition de l'objet window
  /* on ne peut utiliser window.a = 1 ou window['a'] = 1 */
  /* on pourrait utiliser a = 1 (sans instruction var) pour la déclarer globale */
  function setGlobal(key, value) { this[key] = value; }
  setGlobal('a', 1); // la variable "a" est définie globalement
  console.log(a); /* affiche 1 */
}
new f();
console.log(a, window.a); /* affiche 1, 1 */
[voir exemple 5]

function scope / visibilité dans les fonctions

L'instruction "var" dans une fonction permet de définir une variable avec une visibilité restreinte à la fonction et les fonctions définies (imbriquées) dans la fonction. L'instruction "var" peut être n'importe où dans la fonction et sera lu avant l'exécution du code de la fonction (voir l'exemple 3).

function f() {
 var a = 1;
 console.log(a);

 function g() {
   /* g est définie/imbriquée dans f() et peut donc utiliser a */
   console.log(a);
 }
 g();
}
f();

/* a n'est pas accessible ici */
try {
  console.log(a); /* n'affiche rien */
} catch(e) {
  console.log("exception: la variable 'a' n'est pas accessible.")
}
[voir exemple 6]

Limitation: il n'existe pas d'objet prédéfini contenant les variables déclarées avec l'instruction "var" dans une fonction comme il existe l'objet "window" pour les variables globales. On peut toutefois déclarer un objet qui contient des propriétés. Au lieu d'avoir var a = 1, b = 2, c = 3, d = 4; on pourrait avoir: var myvar = { a: 1, b: 2, c: 3, d: 4} et utiliser myvar['a'] ou for (var i in myvar) { console.log(myvar[i]); };

L'instruction "var" permet de redéfinir une nouvelle variable qui existe dans une fonction parente ou dans une visibilité plus large.

function f() {
 var a = 1;
 console.log(a); /* affiche 1 */

 function g() {
   var a = 2; /* ce "a" est une nouvelle variable avec une visibilité restreinte à la fonction g() */
   console.log(a); /* affiche 2 */
 }
 g();
 console.log(a); /* affiche 1 */
}
f();
[voir exemple 7]

Note: on ne peut pas accéder à la variable "a" de la fonction f() à l'intérieur de g(). Il faut choisir un autre nom de variable.

Les arguments d'une fonction possède aussi une visibilité propre à la fonction et aux fonctions imbriquées tout comme l'instruction "var". L'argument est une nouvelle variable (zone mémoire).

function f(a) {
 console.log(a);  /* affiche 1 */

 function g() {
   /* g est définie/imbriquée dans f() et peut donc utiliser a */
   console.log(a); /* affiche 1 */
 }
 g();
}
f(1);

/* a n'est pas accessible ici */
try {
  console.log(a); /* n'affiche rien */
} catch(e) {
  console.log("exception: la variable 'a' n'est pas accessible.")
}
[voir exemple 8]

On peut déclarer une variable avec le même nom qu'un argument. L'instruction "var" ne fait rien et l'affectation de la variable agit normalement.

function f(a) {
 console.log("example9 f()", a, "before var"); /* affiche 1 */
 var a = 2;
 console.log("example9 f()", a, "after var"); /* affiche 2 */

 function g(a) {
   console.log("example9 f()", a, "before var"); /* affiche 3 */
   var a = 4; /* ce "a" est une nouvelle variable avec une visibilité restreinte à la fonction g() */
   console.log("example9 g()", a, "after var"); /* affiche 4 */
 }
 g(3);
 console.log("example9 f()", a, "after g()"); /* affiche 2 */
}
f(1);
[voir exemple 9]

Même avec un objet, le comportement est le même que si on n'utilise pas l'instruction "var".

function f(o) {
 o.a = 1;
 console.log("example10 f()", o.a, "before var"); /* affiche 1 */
 var o = 2;
 console.log("example10 f()", o, "after var"); /* affiche 2 */
}
var obj = { a: 0 };
f(obj);
console.log("example10 global", obj.a); /* affiche 1 */
[voir exemple 10]

block scope / visibilité par bloc - javascript 1.7

La nouvelle instruction "let" permet de restreindre la visibilité d'une variable à un bloc if, for, while, do while, for each, {}...

Il faut absolument spécifier la version de javascript, soit <script type="text/javascript;version=1.7>

function f() {
 var a = 1;
 console.log("example11 f()", a, "before let"); /* affiche 1 */
 {
   let a = 2;
   console.log("example11 f()", a, "inside the block scope"); /* affiche 2 */
   function g() {
     console.log("example11 g()", a, "inside the block scope"); /* affiche 2 */
   }
   g();
 }
 console.log("example11 f()", a, "after the block scope"); /* affiche 1 */
}
f();
[voir exemple 11]

Cas pratique

Avec les scripts d'animation (setTimeout/setInterval), les évènements, les requêtes asynchrones (ajax) et les nouvelles méthodes (ie: Array.prototype.map) utilisant des fonctions de rappel (callback), il est important de comprendre l'importance de la visibilité des variables.

Posons le problème suivant: nous voulons afficher les nombres de 10 à 14 mais en utilisant une fonction de rappel. On fait une boucle de i = 10 jusqu'à 14, on stocke les fonctions de rappel dans une liste. Après la boucle, on appel les fonctions une par une. Si on ne tient pas compte de la visibilité des variables on peut avoir des surprises. Stocker les fonctions de rappel, utiliser un setTimeout ou utiliser la fonction avec ajax, le phénomène reste le même, on crée des fonctions dans une boucle et on utilise les fonctions de rappel (callback) après la boucle. Dans cet exemple incorrect, la valeur "15" sera affiché 5 fois.

(function() {

var list = [];

/* ajoute des fonctions dans une liste */
for(var i = 10; i < 15; i++) {
  var f = function() { console.log(i); }
  list.push( f );
}

/* la valeur de i est 15 à la fin de la boucle */
list[0](); /* première fonction, mais i est 15, donc affiche 15 */
list[1](); /* deuxième fonction, mais i est 15, donc affiche 15 */
list[2](); /* troisième fonction, mais i est 15, donc affiche 15 */
list[3](); /* quatrième fonction, mais i est 15, donc affiche 15 */
list[4](); /* cinquième fonction, mais i est 15, donc affiche 15 */

})();
[voir cas 1]

Pour éviter ce genre de problème et perdre la valeur de "i" lorsqu'on a crée la fonction, on peut utiliser la visibilité d'un argument dans une fonction (function scope). L'argument est une nouvelle variable avec la valeur envoyée.

var a = 1;
function f(b) {
  console.log(b); /* affiche 1, la valeur définie dans l'argument est 1 */
  b = 2;
  console.log(b); /* affiche 2, le changement est seulement dans la fonction, et pas à l'extérieur de la fonction */
}
f(a);
console.log(a); /* affiche 1 */
[voir l'exemple 12]

En utilisant ce comportement, on peut créer des nouvelles portées/visibilités. Il suffit de créer une fonction avec un argument qui retourne une fonction de rappel. Voici le cas 1 corrigé:

(function() {
var list = [];

/* ajoute des fonctions dans une liste */
for(var i = 10; i < 15; i++) {

  /* l'argument "a" est une nouvelle variable à chaque fois qu'on appel la fonction et n'est pas relié à "i" même si a contient la valeur de "i" lors de l'appel */
  function genScope(a) {
    return function() { console.log(a); }
  }
  var f = genScope(i); 
  /* on aurait pu écrire : var f = (function(a) { return function() { console.log(a); } })(i); */
  list.push( f );
}

/* la valeur de i est 5 à la fin de la boucle */
list[0](); /* première fonction, l'argument "a" avait la valeur 10 à la création de la fonction, donc affiche 10 */
list[1](); /* deuxième fonction, l'argument "a" avait la valeur 11 à la création de la fonction, donc affiche 11 */
list[2](); /* troisième fonction, l'argument "a" avait la valeur 12 à la création de la fonction, donc affiche 12 */
list[3](); /* quatrième fonction, l'argument "a" avait la valeur 13 à la création de la fonction, donc affiche 13 */
list[4](); /* cinquième fonction, l'argument "a" avait la valeur 14 à la création de la fonction, donc affiche 14 */
})();
[voir cas 2]