8 janvier 2015

Javascript : Scope des variables


En Javascript, il est important de maîtriser le scope des variables pour éviter les effets de bord et maîtriser l'utilisation de closures.

Voici un rapide résumé des règles du scope des variables globales :
  • une variable est une variable globale si elle déclarée pour la première fois sans utiliser le mot clé var, que l'on soit ou non dans le code d'une fonction
  • une variable globale est accessible et modifiable de partout 
Voici un rapide résumé des règles du scope des variables d'une fonction :
  • une variable est dans le scope d'une fonction si cette variable a été déclarée via le mot clé var
  • le paramètre d'une fonction est dans le scope des variables de cette fonction : il s'agit du même comportement que pour une variable définie via le mot clé var
  • une variable dans le scope d'une fonction est accessible et modifiable depuis le code de cette fonction ainsi que depuis les fonctions enfants de cette fonction
  • une variable du scope d'une fonction parent peut être masquée dans une fonction enfant par une variable de même nom dans le scope de cette fonction enfant

Règle de survie en Javascript :
Le comportement des variables globales est dangereux et provoque des effets de bord très difficiles à corriger, c'est pourquoi il faut toujours utiliser le mot clé var pour déclarer une variable.

Variable globale VS Variable dans le scope d'une fonction

Par défaut, une variable est globale, c'est à dire qu'elle est accessible et modifiable de partout.

Le mot clé var indique que la variable est dans le scope de la fonction dans laquelle elle a été déclarée.

console.log('a', typeof a === 'undefined'); // true : a n'est pas défini
console.log('b', typeof b === 'undefined'); // true : b n'est pas défini

function test() {
  var a = 1; // a est une variable dans le scope de la fonction "test"
  b = 2; // b devient une variable globale
}

test();

// a n'est plus accessible car on est sorti de la fonction "test"
console.log('a', typeof a === 'undefined'); // true : a n'est pas défini

// b est une variable globale
console.log('b', typeof b === 'undefined'); // false : b est défini : b = 2
console.log('b', b); // b = 2 

La variable a a été définie via le mot clé var et est donc une variable définie dans le scope de la function test. La variable a n'est donc pas accessible à l'extérieur de la fonction test où elle a été définie pour la première fois.

Cependant b est devenu une variable globale car elle a été définie pour la première fois dans la fonction sans utiliser le mot clé var. Par conséquent, elle reste définie et accessible à l'extérieur de la fonction test.

Je répète la règle suivante pour éviter les effets de bord en Javascript :
Le comportement des variables globales est dangereux et provoque des effets de bord très difficiles à corriger, c'est pourquoi il faut toujours utiliser le mot clé var pour déclarer une variable.

Héritage des scopes des fonctions parents

Une fonction déclarée au sein d'une fonction parent hérite du scope de variables de cette fonction parent.

Si une variable est déclarée dans le scope d'une fonction enfant et que cette variable a le même nom qu'une variable dans le scope de la fonction parent, alors la variable du scope de la fonction enfant masque la variable du scope de la fonction parent.

Exemple :
function parent_1() {
 var a = 1;
 var b = 1;
 var c = 1;
 
 console.log('\n parent :');
 console.log('a : ',a); // a : 1 => scope de "parent_1"
 console.log('b : ',b); // b : 1 => scope de "parent_1"
 console.log('c : ',c); // c : 1 => scope de "parent_1"
 
 function child_1() {
  var b = 2;
  var c = 2;

  console.log('\n child_1 :');
  console.log('a : ',a); // a : 1 => scope de "parent_1"
  console.log('b : ',b); // b : 2 => scope de "child_1"
  console.log('c : ',c); // c : 2 => scope de "child_1"
  
  function child_1_1() {
   var c = 3;
  
   console.log('\n child_1_1 :');
   console.log('a : ',a); // a : 1 => scope de "parent_1"
   console.log('b : ',b); // b : 2 => scope de "child_1"
   console.log('c : ',c); // c : 3 => scope de "child_1_1"
  }
  child_1_1();
 }
 child_1();
}
parent_1();

Les paramètres d'une fonction sont dans le scope de cette fonction

Les paramètres d'une fonction sont dans le scope des variables de cette fonction.

Nous avons ainsi le même scope et comportement que la variable soit un paramètre de la fonction ou une variable déclarée via le mot clé var dans cette fonction.

En reprenant l'exemple précédent d'héritage de scope, nous avons le même comportement avec les paramètres des fonctions :
function parent_1(a, b, c) {
 
 console.log('\n parent :');
 console.log('a : ',a); // a : 1 => scope de "parent_1"
 console.log('b : ',b); // b : 1 => scope de "parent_1"
 console.log('c : ',c); // c : 1 => scope de "parent_1"
 
 function child_1(b, c) {
 
  console.log('\n child_1 :');
  console.log('a : ',a); // a : 1 => scope de "parent_1"
  console.log('b : ',b); // b : 2 => scope de "child_1"
  console.log('c : ',c); // c : 2 => scope de "child_1"
  
  function child_1_1(c) {
  
   console.log('\n child_1_1 :');
   console.log('a : ',a); // a : 1 => scope de "parent_1"
   console.log('b : ',b); // b : 2 => scope de "child_1"
   console.log('c : ',c); // c : 3 => scope de "child_1_1"
  }
  child_1_1(3);
 }
 child_1(2, 2);
}
parent_1(1, 1, 1);

Closures

Le mode de fonctionnement des scopes de variables dans les fonctions permet la déclaration de closures :

  • une fonction enfant définie dans une fonction parent conservera toujours un lien vers les variables du scope de cette fonction parent
Ainsi, une fois que la fonction enfant a été déclarée, elle aura toujours accès aux variables du scope de la fonction parent, même si cette fonction enfant est appelée par du code située à l'extérieur de la fonction parent

Le fait que la fonction enfant conserve les variables du scope de la fonction parent permet de définir et de réutiliser ces variables définies à un instant donné dans le passé. Ceci permet de se constituer un sorte de contexte de variables et de valeurs réutilisables dans le futur.

Ce fonctionnement permet par exemple l'écriture de fonctions callback utilisées par exemple en JQuery avec les listener pour gérer les réponses aux événements utilisateur. En effet, la fonction callback conserve les variables telles que lors de sa définition et donc les liens vers les éléments auxquelles elle est liée.

Exemple 1

Dans l'exemple ci-dessous, la fonction func_child conserve un lien vers les variables définis dans le scope de la fonction parent func_parent. Lorsque func_child est appelée depuis du code situé à l'extérieur de la fonction parent func_parent, les valeurs des variables à la déclaration de la fonction func_child ont été conservées.
function func_parent(a, b) {
  var c = 3;
  var d = 4;

  var func_child = function() {
    return a+b+c+d;
  }

  return func_child;
}

var func_child_1 = func_parent(1,2);
var func_child_2 = func_parent(11,12);

console.log('func_child_1',func_child_1()); // résultat : a+b+c+d => 1+2+3+4 => 10
console.log('func_child_2',func_child_2()); // résultat : a+b+c+d => 11+12+3+4 => 30

Exemple 2

Autre exemple : on utilise une variable du scope de la fonction parent comme compteur pour la fonction enfant qui sera incrémenté à chaque appel de la fonction enfant. On déclare deux fonctions enfants func_child_1 et func_child_2 à partir de deux appels différents à la fonction parent afin d'avoir deux scopes de variables différents et donc deux compteurs différents.
function func_parent(msg) {
  var counter = 0;

  var func_child = function() {
    counter += 1;
    return msg + counter;
  }

  return func_child;
}

var func_child_1 = func_parent('func_child_1 : counter = ');
var func_child_2 = func_parent('func_child_2 : counter = ');

console.log(func_child_1()); // 'func_child_1 : counter = 1'
console.log(func_child_1()); // 'func_child_1 : counter = 2'
console.log(func_child_2()); // 'func_child_2 : counter = 1'
console.log(func_child_1()); // 'func_child_1 : counter = 3'
console.log(func_child_2()); // 'func_child_2 : counter = 2'

Conclusion

En Javascript, il est important de bien comprendre le fonctionnement du scope des variables dans les fonctions pour maîtriser l'utilisation des closures.