La portée (i.e. scope) d’un objet est la portion de code dans laquelle une variable peut exister et maintenir une valeur qui lui aura été préalablement affectée. La portée des objets est loin d’être un sujet trivial en Javascript car suivant la façon dont on déclare les objets, beaucoup de règles ou de comportements du langage ne sont pas forcément intuitifs et surtout sont opposés à ceux d’autres langages plus standards comme le C++, C#, Java etc…
Le but de cet article est d’essayer d’indiquer tous les cas atypiques de comportements de la gestion de la portée des objets en Javascript pouvant mener à des erreurs d’implémentations.
Dans un premier temps, on va indiquer les règles générales liées à la portée suivant l’utilisation de var
, let
ou const
. Ensuite, on montrera quelle syntaxe permet de modifier les règles liées à la gestion de la portée par le moteur Javascript. Dans un 3e temps, on développera les différents comportements du mot-clé this
. Enfin, on indiquera quelles sont les règles liées à l’hoisting.
Portée statique vs portée dynamique
var, let et const
Portée avec let et const
Portée avec var
Comment modifier le comportement de la portée statique ?
eval()
Comportement de eval() avec le mode strict
Comportement de eval() avec const et let
new Function()
this
Arrow function
Comment affecter une valeur à this ?
call() et apply()
bind()
setTimeout()
Closure
Qu’est ce qu’une closure ?
Conséquences avec this
Pour déclarer des variables en Javascript de façon à éviter de déclarer de façon globale, on utilise les mots-clé var
ou let
(ou const
dans le cas d’une variable dont la valeur est constante). Suivant le mot-clé utilisé, le comportement du moteur Javascript sera très différent. Avec let
et const
, les comportements sont assez standards pour rapport à d’autres langages. En revanche, avec var
, les comportements sont spécifiques et peuvent surprendre en particulier si on est habitué à d’autres langages.
On pourrait se demander quelle est la justification qui nécessite de s’intéresser à var
, il suffirait d’utiliser uniquement let
. La raison est que let
est apparu avec ES6 (ES2015) et que beaucoup d’applications fonctionnent avec un code compatible ES5 utilisant var
.
Variables globales
Un des plus gros inconvénient de Javascript est de permettre de déclarer des variables globales. La portrée de ces variables étant celui de l’application, elles garderont leur valeur dans toute l’application. Ainsi une même variable peut être valuée à des endroits différents du code et des conflits peuvent apparaître entre ces valuations dans le cas où elles sont incompatibles ce qui peut mener à des erreurs. D’une façon générale, il faut éviter d’utiliser des variables globales.
Les variables globales se déclarent de cette façon:
<nom variable> = <valeur>;
ou
window.<nom variable> = <valeur>;
Par exemple:
variable = 'OK';
ou
window.variable = 'OK';
Portée statique vs portée dynamique
Quand Kyle Simpson présente la portée des objets en Javascript dans You don’t know JS, il explique le comportement de Javascript en passant par la portée statique (i.e. lexical scope).
La portée statique ou portée lexicale (i.e. static scope ou lexical scope) s’oppose à la portée dynamique (i.e. dynamic scope). Il s’agit d’un ensemble de règles utilisé par le moteur d’exécution d’un langage pour identifier une variable nommée et pour en déterminer la valeur. La portée statique est le comportement utilisé par la plupart des langages y compris C++, C# ou Java. Javascript utilise aussi cet ensemble de règles.
Dans la gestion de la portée des variables, tous ces langages ont des comportements similaires:
- Les portées s’imbriquent: une portée peut se trouver à l’intérieur d’un autre portée. Les variables définies dans la portée parente sont accessibles dans la portée enfant.
- La portée la plus générale est la portée globale: dans le cas de Javascript, une variable globale est accessible partout.
- La plupart du temps, une portée ne peut pas appartenir à plus d’une seule portée parente.
En Javascript, les règles qu’imposent la portée statique sont les suivantes:
- Règle 1: lorsque le moteur d’exécution exécute la déclaration d’une variable, la portée de la variable sera celle dans laquelle se trouve la déclaration.
- Règle 2: lorsque le moteur d’exécution cherche la valeur d’une variable, il recherche sa déclaration dans la portée courante. Si aucune déclaration n’est trouvée, le moteur cherche dans les portées parentes. Si aucune déclaration n’est trouvée alors la variable appartient à la portée globale.
- Règle 3: si une variable est déclarée dans une portée parente et qu’une variable est déclarée avec le même nom dans une portée enfant, la variable dans la portée parente sera occultée par celle de la portée enfant. Ce comportement correspond au shadowing.
Pour se rendre compte de la différence entre la portée statique et la portée dynamique, on peut prendre l’exemple suivant:
var x = 1;
function a() {
console.log('x in a(): ',x); // x => 1
x = 2;
}
function b() {
var x = 3; // x => 3
a();
console.log('x in b(): ',x); // x => 3
}
b();
console.log(x); // x => 2
Suivant la portée utilisée, le comportement est le suivant:
Portée statique (i.e. lexical scope) |
Portée dynamique (i.e. dynamic scope) |
La portée dynamique utilise des piles durant l’exécution pour garder les valeurs des variables de façon dynamique. Javascript a le comportement correspondant à la portée statique, il n’utilise donc pas la portée dynamique.. |
|
|
|
Les 2 comportements sont donc différents et impliquent des valeurs différentes suivants les portées dans lesquelles les affectations et les récupérations de valeurs sont faites.
var, let et const
Les mots-clé var
, let
et const
permettent de déclarer une variable en limitant sa portée: suivant le mot-clé utilisé, la portée de la variable sera très différente:
var
permet de déclarer une variable dans la portée globale ou dans la portée d’une fonction.const
etlet
permettent de déclarer une variable dans la portée d’un bloc de code.
Déclarer une variable avec ces mots-clé est possible avec la syntaxe suivante:
var <nom variable>;
let <nom variable>;
Avec const
, une initialisation est obligatoire.
Pour initialiser une variable en même temps que sa déclaration, la syntaxe est:
var <nom variable> = <valeur d'initialisation>;
let <nom variable> = <valeur d'initialisation>;
const <nom variable> = <valeur d'initialisation>;
Portée avec let et const
La gestion de la portée est similaire entre let
et const
. La différence entre ces mots-clé est que:
let
autorise des affectations d’une nouvelle valeur à une variable.const
interdit de nouvelles affectations.
Si on exécute:
const a = 3;
a = 5; // SyntaxError
On obtient une erreur de type 'SyntaxError: redeclaration of const a'
.
let
et const
sont apparus à partir de ES6 et ont pour but d’avoir un comportement similaire à la plupart des autres langages: ils limitent la portée d’une variable à un bloc de code. Un bloc de code étant la portion de code délimitée par { ... }
, par exemple:
- Une fonction:
function add(a, b) { // ... }
- Une clause
if...else
,for
etc:for (let i = 0; i < 10; i++) { // ... }
- Le contenu des blocs dans un
try...catch
:try { // ... } catch (err) { // err est limité au bloc catch }
- Une bloc simple:
{ // ... }
- Une bloc nommé:
namedBlock: { // ... }
- Un objet:
var obj = { // ... };
Ainsi avec let
ou const
, la portée d’une variable est limitée au bloc de code dans laquelle elle est définie et dans les blocs enfant.
Le comportement est similaire à la plupart des autres langages, une variable déclarée dans un bloc de code ne sera pas accessible en dehors de ce bloc et de ses blocs enfant (sauf en cas de shadowing).
Par exemple:
const a = 'OK';
if (a === 'OK') {
let b = 5;
console.log('Inside if: a=',a,' ;b=',b); // 'Inside if: a=OK ;b=5'
}
console.log('Outside if: a=',a); // le résultat est 'Outside if: a=OK'
console.log('Outside if: b=',b): // Reference Error
Dans le code plus haut, on peut voir 2 blocs de code: celui en dehors de la clause if
et celui en dehors. Ainsi:
- La variable
a
a été définie dans le bloc en dehors de la clauseif
, elle est donc disponible dans les 2 blocs. - La variable
b
a été définie dans le bloc duif
, elle n’est accessible que dans ce bloc.
Portée avec var
Contrairement à let
et const
, la portée des objets avec var
correspond aux fonctions. Les blocs de code ne limitent pas les portées quand on déclare une variable avec var
.
Par exemple, si on considère le code suivant:
var a = 'OK';
if (a === 'OK') {
var b = 5;
console.log('Inside if: a=',a,' ;b=',b);
}
console.log('Outside if: a=',a,' ;b=',b);
La portée est la même pour les variables a
et b
même si b
est déclarée dans le corps de la clause if
. A l’intérieur ou à l’extérieur de la clause if
, le résultat est le même:
Inside if: a=OK ;b=5
Outside if: a=OK ;b=5
La notion de portée n’est pas modifiée suivant le type de fonction. Dans le cas de var
, la portée des variables correspond aux fonctions.
Si on considère le code suivant:
var x = 3;
function namedFunction() {
var x = 2;
console.log(x); // 2
}
namedFunction();
console.log(x); // 3
La règle de la portée statique s’applique dans le cadre de cet exemple.
- Fonction anonyme (i.e. function expression): le comportement est le même pour une fonction anonyme:
var x = 3; var anonymousFunction = function() { var x = 2; console.log(x); // 2 } anonymousFunction(); console.log(x); // 3
- IIFE (i.e. Immediatly Invoked Function Expression) qui sont exécutées au même moment que leur déclaration:
var x = 3; (function() { var x = 2; console.log(x); // 2 })(); console.log(x); // 3
- Arrow function: à partir de ES6:
var x = 3; var arrowFunction = () => { var x = 2; console.log(x); // 2 })(); arrowFunction(); console.log(x); // 3
Comment modifier le comportement de la portée statique ?
Il est possible de modifier le comportement de la portée statique en utilisant eval()
ou le constructeur new Function()
.
eval()
eval()
permet d’évaluer une expression sous forme de chaîne de caractères.
Par exemple:
let result = eval('1 + 3');
console.log(result); // 4
Ainsi eval()
exécute l’expression sous forme de chaîne de caractères: '1 + 3'
.
La portée d’une variable utilisée dans une expression évaluée par eval()
dépend de la portée dans laquelle l’expression est évaluée et non en fonction de la portée dans laquelle la chaîne évaluée est construite.
Par exemple, si on exécute:
function a(expr) {
eval(expr);
console.log('x=',x); // 4
}
var x = 3;
let expr = 'var x = 4';
a(expr);
console.log('x=',x); // 3
La portée utilisée pour évaluer x
avec eval()
est celui de la fonction a()
. Même si l’expression expr
a été construite à l’extérieure de a()
, c’est la portée dans laquelle eval()
est exécuté qui sera utilisé.
Dans cet exemple, quand expr
est évaluée dans le corps de la méthode a()
, var x = 4
occulte la première déclaration de x
pour définir une 2e variable nommée x
dont la valeur est 4
. À l’extérieur de a()
, la variable x
correspond à la variable déclarée avec var x = 3
.
Comportement de eval() avec le mode strict
Si on utilise le mode strict, l’expression évaluée dans eval()
possède une portée spécifique qui ne déborde pas de la portée dans laquelle eval()
est exécutée.
Par exemple si on exécute le code suivant:
function a(expr) {
"use strict";
eval(expr); // Inside eval: 4
console.log('x=',x); // 3
}
var x = 3;
let expr = "var x = 4; console.log('Inside eval: ',x)";
a(expr);
console.log('x=',x); // 3
Toutes les évaluation de x
en dehors de eval()
possède la valeur 3
car la variable utilisée est celle déclarée par var x = 3
. Toutefois avec le mode strict, la portée de l’expression évaluée par eval()
est limité, ainsi x
possède la valeur 4
seulement dans l’expression évaluée.
Comportement de eval() avec const et let
Si on utilise const
ou let
pour déclarer une variable dans une expression évaluée par eval()
, la portée est spécifique à l’expression évaluée, il ne déborde pas dans la portée dans laquelle eval()
est exécutée.
Par exemple, si on exécute le code suivant:
function a(expr) {
eval(expr); // Inside eval: 4
console.log('x=',x); // 3
}
let x = 3;
let expr = "let x = 4; console.log('Inside eval: ',x)";
a(expr);
console.log('x=',x); // 3
Toutes les évaluation de x
en dehors de eval()
possède la valeur 3
car la variable utilisée est celle déclarée par var x = 3
. La portée de l’expression évaluée par eval()
est limitée, x
possède la valeur 4
seulement dans l’expression évaluée.
new Function()
La constructeur Function()
permet de déclarer un objet de type Function
. Utiliser ce constructeur permet d’évaluer une expression de la même façon qu’avec eval()
. La syntaxe à utiliser est:
var objectFunction = new Function([arg1, arg2, ..., argN], expression);
Dans cet appel, expression
contient la chaîne de caractères à évaluer.
On peut exécuter la fonction de cette façon:
objectFunction(arg1, arg1, ..., argN, '....');
Du point de vue de la portée, cet objet ne se comporte ni comme une fonction normale, ni comme eval()
: la portée parente d’un objet de type Function
est la portée globale et non la portée de la fonction parente.
Par exemple dans le cadre général, si une fonction est définie dans une autre fonction dite parente, la portée parente de la fonction est la portée de la fonction parente. C’est la règle de la portée statique.
Ainsi, si on exécute le code suivant:
function parent() {
function a() {
x = 2;
console.log('x=',x); // x=2
}
var x = 3;
a();
console.log('x=',x,'window.x=',window.x); // x=2 window.x=undefined
}
parent();
La règle de la portée statique s’applique pour déterminer qu’elle est la déclaration d’une variable:
- Dans le corps de la fonction
a()
:x
désigne la variable déclarée avecvar x = 3
donc partoutx
désigne la même variable. - Dans le corps de
parent()
:window.x
est indéfinie puisqu’on a jamais affecté de valeur à la variable globalex
.
Si on considère un code semblable utilisant un objet de type Function
:
function parent() {
var a = new Function("x = 2; console.log('x=',x);");
var x = 3;
a(); // x= 2
console.log('x=',x,'window.x=',window.x); // x=3 window.x=2
}
parent();
La portée parente de l’objet Function
n’est pas la fonction parent()
mais la portée globale, ainsi:
- Quand l’expression est évaluée
x = 2
fera référence àx
dans la portée parente c’est-à-dire la portée globale. C’est la raison pour laquellewindow.x
renvoie la valeur2
. - L’expression évaluée dans le constructeur de
Function()
modifiex
en tant que variable globale donc la variable déclarée parvar x = 3
n’est pas modifiée etconsole.log('x=',x)
dans le corps deparent()
renvoie3
.
De même, la portée parente de eval()
correpond, par défaut, à la portée dans laquelle eval()
est exécutée. Ce qui est contraire au comportement de l’objet Function
puisque sa portée parente est la portée globale.
this
this
est un mot-clé très courant dans beaucoup de langage pour désigner l’instance courante d’une classe.
En Javascript, c’est aussi le sens de ce mot-clé dans le cadre des classes introduites à partir de ES6.
Par exemple:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
logPerson() {
console.log(this.firstName, this.lastName);
}
}
var person = new Person('Uncle', 'Bob');
person.logPerson(); // Uncle Bob
Dans cet exemple, this
renvoie bien à l’instance courante de la classe Person
.
En Javascript, le plus souvent this
est utilisé dans un cadre différent des classes. Au lieu de représenter l’instance d’une classe, il désigne un contexte dans lequel le code est exécuté. Suivant les cas d’usage de this
, sa valeur peut être plus difficile à prévoir que dans le cas d’une classe.
Parmi les cas les plus courants d’utilisation de this
, il y a celui du corps d’une fonction (cette fonction n’appartient pas à une classe ou à un objet). Dans le cas d’une fonction:
- En mode non strict:
this
désigne le contexte global. - En mode strict:
this
est indéfini sauf si on indique explicitement sa valeur.
Par exemple:
function a() {
return this;
}
console.log(a() === window); // true
Dans ce cas, this
désigne le contexte global.
Dans la cas du mode strict, this
est indéfini car sa valeur n’a pas été indiquée explicitement:
function a() {
"use strict";
return this;
}
console.log(a()); // undefined
Dans le cas d’un objet, this
dans une fonction désigne implicitement cet objet:
var obj = {
x: 5,
a: function() {
return this;
}
};
console.log(obj.a().x === 5); // true
Dans ce cas, this
désigne l’objet obj
.
Arrow function
D’une façon générale, les arrow functions ont les mêmes comportements que les fonctions normales c’est-à-dire que this
désigne le contexte global ou l’objet englobant.
Par exemple, si on considère l’exemple suivant:
var arrowFunction = () => this; // Référence d'une arrow function
console.log(arrowFunction() === window); // true
Dans le cas de l’exemple précédent, on exécute dans le contexte global donc this
dans l’arrow function désigne le contexte global.
Dans le cas d’une arrow function déclarée dans un objet, il faut distinguer l’appel à une fonction et le cas d’une référence vers une fonction.
Par exemple, si on place cette fonction dans un objet:
var obj = {
x: 5,
a: function() {
var arrowFunction = () => this;
return arrowFunction; // On retourne la référence de la fonction
}
};
var fn = obj.a(); // On récupère la référence de l'arrow function dans le contexte de l'objet obj.
console.log(fn().x === 5); // True
console.log(obj.a().x === 5); // False
console.log(obj.a()().x === 5); // True
Ainsi:
console.log(fn().x === 5)
retournetrue
carfn
est une référence vers l’arrow function dans le contexte de l’objetobj
. Quand on exécutéfn()
, on exécute l’arrow function etthis
désigne l’objetobj
. Par suiteobj.x
contient la valeur5
.console.log(obj.a().x === 5)
retournefalse
carobj.a()
contient la référence de l’arrow function, il faut exécuterobj.a()()
pour exécuter réellement l’arrow function.
Comment affecter une valeur à this ?
On peut affecter une valeur à this
avec les fonctions call()
, apply()
ou bind()
.
call() et apply()
call()
et apply()
permettent de contrôler la valeur de this
en indiquant quel est le contexte d’exécution d’une fonction.
La syntaxe de ces méthodes est:
call()
:<fonction>.call(thisVar, [arg1, arg2,..., argN]);
Avec
<fonction>
la fonction à exécuter;thisVar
, la valeur dethis
et[arg1, arg2,..., ArgN]
, les arguments de la fonction à exécuter.
En mode non strict, sithisVar
est nul, alorsthis
contient le contexte global.apply()
:<fonction>.apply(thisVar, [tableau d'arguments]);
Avec
<fonction>
la fonction à exécuter;thisVar
, la valeur dethis
et[tableau d'arguments]
, les arguments de la fonction à exécuter sous forme d’un tableau.
En mode non strict, sithisVar
est nul, alorsthis
contient le contexte global.
Si on considère l’exemple suivant:
function a(z, w) {
return this.x + this.y + z +w;
}
var obj = {
x: 5,
y: 6
};
x = 1;
y = 2;
console.log(a.call(null, 3, 4)); // 10
console.log(a.apply(null, [3, 4])); // 10
Les 2 appels précédents permettent d’exécuter a()
dans le contexte global puisque la valeur indiquée de this
est nulle. Dans le contexte global, x
et y
contiennent respectivement les valeurs 1
et 2
.
Si on exécute:
console.log(a.call(obj, 3, 4)); // 18
console.log(a.apply(obj, [3, 4])); // 18
this
contient obj
donc a()
est exécuté dans le contexte de obj
. x
et y
contiennent respectivement les valeurs 5
et 6
.
Si on modifie le code pour activer le mode strict:
function a(z, w) {
"use strict";
return this.x + this.y + z +w;
}
var obj = {
x: 5,
y: 6
};
x = 1;
y = 2;
console.log(a.call(null, 3, 4)); // TypeError: this is null
console.log(a.apply(null, [3, 4])); // TypeError: this is null
Si le mode strict est activé et que this
contient null
, sa valeur n’est pas remplacée par le contexte global. a()
ne peut pas effectuer l’addition car this
est nul.
bind()
A partir de ES5, bind()
permet aussi de préciser une valeur de this
en générant une référence vers une nouvelle fonction. Cette nouvelle fonction sera liée au contexte indiqué lors de l’exécution de bind()
.
La syntaxe de bind()
est:
let <référence fonction> = <fonction>.bind(thisVar, [arg1, arg2,..., argN]);
Avec <fonction>
la fonction à exécuter; thisVar
, la valeur de this
et [arg1, arg2,..., ArgN]
, les arguments de la fonction à exécuter.
Avec bind()
, le cas d’usage est un peu différent de call()
et apply()
. Si on reprend l’exemple précédent:
function a(z, w) {
return this.x + this.y + z +w;
}
var obj = {
x: 5,
y: 6
};
x = 1;
y = 2;
var boundA = a.bind(null, 3, 4); // On définit une nouvelle référence sans indiquer de valeur pour this
console.log(boundA()); // 10
Si on ne précise pas de valeur pour this
, c’est le contexte global qui est utilisé. Dans cet exemple, dans le corps de a()
, this
fait référence au contexte global, x
et y
ont pour valeur, respectivement, 1
et 2
.
Si on indique une autre valeur pour this
:
var boundA = a.bind(obj, 3, 4);
console.log(boundA()); // 18
Dans cet appel, this
fait référence à l’objet obj
donc x
et y
ont pour valeur, respectivement 5
et 6
.
Enfin dans le cas du mode strict, si on ne précise pas de valeur pour this
, sa valeur restera nulle. Si on reprend l’exemple précédent:
function a(z, w) {
"use strict";
return this.x + this.y + z +w;
}
var obj = {
x: 5,
y: 6
};
x = 1;
y = 2;
var boundA = a.bind(null, 3, 4);
console.log(boundA()); // TypeError: this is null
Avec le mode strict, this
est nul dans le corps de la fonction a()
d’où l’erreur.
setTimeout()
Avec setTimeout()
, la valeur par défaut de this
est le contexte global. Ce comportement peut prêter à confusion quand on utilise this
dans une fonction exécutée par setTimeout()
.
Par exemple:
var obj = {
x: 5,
a: function() {
console.log(this.x);
}
};
var x = 3;
obj.a(); // 5
Dans ce dernier appel, this
fait référence à l’objet obj
puisque a()
se trouve dans cet objet.
En revanche si on exécute le code suivant, this
fait référence au contexte global:
setTimeout(obj.a, 100); //3
C’est la 3
qui s’affiche car c’est la variable x
déclarée dans le contexte global qui est utilisée.
La solution peut consister à utiliser une arrow function (disponible à partir de ES6):
setTimeout(() => obj.a(), 100); // 5
Cet appel permet de forcer l’appel dans le contexte de l’objet obj
.
On peut aussi utiliser bind()
(à partir de ES5) pour indiquer explicitement la valeur de this
:
var boundA = obj.a.bind(obj);
setTimeout(boundA, 100); // 5
Enfin, on peut utiliser une variable locale et encapsuler l’appel à setTimeout
dans la fonction déclarée dans l’objet obj
:
var obj = {
x: 5,
a: function() {
var self = this;
setTimeout(function() {
console.log(self.x);
}, 100);
}
};
var x = 3;
obj.a(); // 5
Closure
Dans le cadre des closures (i.e. fermetures), le comportement de this
peut prêter à confusion car Javascript perd la portée de this
quand il est utilisé dans une fonction qui se trouve dans une autre fonction.
Qu’est ce qu’une closure ?
Avant de rentrer dans les détails de this
dans le cadre d’une closure, on peut rappeler la définition d’une closure (les closures ne sont pas spécifiques à Javascript):
D’après MDN web docs:
“Une fermeture correspond à la combinaison entre une fonction et l’environnement lexical au sein duquel elle a été déclarée. En d’autres termes, une fermeture donne accès à la portée d’une fonction externe à partir d’une fonction interne. En Javascript, une fermeture est créée chaque fois qu’une fonction est créée. “
L’exemple d’une closure qui peut surprendre est celui intervenant dans le cadre d’une boucle:
for (var i = 1; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
A l’exécution, on s’attendrait à voir s’afficher 1
, 2
, 3
, etc… toutefois on voit 5 fois s’afficher 6
. Ce comportement s’explique par le fait que la fonction anonyme se trouvant dans setTimeout()
utilise la portée de la fonction externe, or dans la portée externe i
varie de 1
à 6
. Étant donné que setTimeout()
retarde l’exécution de la fonction anonyme pendant 100 ms, les valeurs affichées correspondent à la dernier valeur de i
.
Pour empêcher ce comportement, il faudrait que la fonction anonyme utilise une variable dont la portée ne dépend pas de la fonction externe. Par exemple, si on utilise une IIFE exécutée à chaque boucle et si on utilise une variable locale à l’IIFE, on pourra utiliser une variable dont la valeur est spécifique à chaque boucle:
for (var i = 1; i < 5; i++) {
// IIFE exécutée à chaque boucle
(function() {
var j = i; // Variable locale
setTimeout(function() {
console.log(j); // On utilise la variable locale
}, 100);
})();
}
A l’exécution, on affiche les différentes valeurs: 1
, 2
, 3
, etc…
Conséquences avec this
Comme on l’a indiqué plus haut:
“En Javascript, une fermeture est créée chaque fois qu’une fonction est créée“.
Ainsi dans le cadre de l’exemple suivant, la closure (i.e. fermeture) entraîne le comportement de la portée statique: pour connaître la valeur d’une variable, le moteur vérifie la portée courante puis les portées parentes jusqu’à ce qu’il trouve la déclaration.
Par exemple:
function a() {
var x = 5;
var nestedA = function() {
console.log(x); // 5
}
nestedA();
}
a();
La valeur affichée sera 5
dans le corps de nestedA()
car cette fonction a accès à la portée de la fonction externe c’est-à-dire celui de a()
. x
étant déclarée dans le corps de a()
alors la valeur affichée sera 5
.
On pourrait s’attendre à ce que le comportement de this
soit identique, par exemple:
var obj = {
x: 5,
a: function() {
console.log(this.x); // 5
var nestedA = function() {
console.log(this.x); // undefined
};
nestedA();
}
};
obj.a();
On pourrait s’attendre à ce que this.x
dans nestedA()
contienne la valeur 5
. Ce n’est pas le cas car la portée de this
est perdue quand une fonction se trouve dans une autre fonction. Dans nestedA()
, this
ne correspond pas à l’objet obj
(comme c’est la cas dans le corps de a()
) mais à la portée globale. Ainsi, this.x
dans nestedA()
est indéfinie.
Le comportement est le même si on utilise une IIFE:
var obj = {
x: 5,
a: function() {
console.log(this.x); // 5
(function() {
console.log(this.x); // undefined
})();
}
};
obj.a();
Par contre avec les arrow functions implémentées à partir de ES6, le comportement est plus standard:
var obj = {
x: 5,
a: function() {
console.log(this.x); // 5
var nestedA = () => {
console.log(this.x); // 5
};
nestedA();
}
};
obj.a();
Dans le cadre des arrow functions, this
correspond bien à l’objet obj
.
Hoisting avec var
L’utilisation de var
pour déclarer des variables entraîne un comportement qui peut prêter à confusion: l’hoisting (i.e. hisser). Le principe de ce comportement est le suivant:
- Le moteur Javascript effectue 2 passes sur le code avant de l’exécuter:
- Une 1ère passe pour référence toutes les déclarations de variables, de fonctions et les définitions des fonctions.
- Une 2e passe permet d’affecter à ces objets leur valeur d’initialisation (s’il y en a une).
- Ces 2 passes entraînent que l’ordre de traitement des lignes de code par le moteur Javascript n’est pas toute à fait le même que celui du script:
- Les déclarations sont traitées avant les affectations (même si dans le script l’affectation d’une variable est placée avant la déclaration).
- Les déplacements des lignes traitées sont effectués à l’intérieur d’une même portée et se limite à la portée d’une fonction.
- Les déclarations de fonction sont déplacées avant les autres objets.
Par exemple, si on exécute le code suivant:
function a() {
x = 5;
var x;
console.log(x); // 5
console.log(window.x); // undefined
}
a();
On pourrait penser qu’étant donné l’ordre des lignes dans a()
, x = 5
correspondrait à l’affectation de 5
à la variable globale x
. Mais le comportement de hoisting entraîne la modification de l’ordre de traitement des lignes par le moteur Javascript. Dans la réalité, les lignes sont exécutées de cette façon:
function a() {
var x;
x = 5;
console.log(x); // 5
console.log(window.x); // undefined
}
a();
A l’intérieur de la portée de la fonction a()
, var x
est traité avant x = 5
.
let
ou const
Si on exécute:
function a() {
x = 5; // ReferenceError: can't access lexical declaration 'x' before initialisation
let x;
console.log(x);
console.log(window.x);
}
a();
Une erreur explicite indique qu’on peut pas inverser l’ordre des lignes, la déclaration doit se trouver avant l’initialisation.
Hoisting valable pour les fonctions anonymes et les arrow functions
Le même comportement d’hoisting est valable pour les fonctions anonymes (i.e. function expressions), par exemple:
a = function() {
return 'from function expression';
}
var a;
console.log(a()); // from function expression
console.log(window.a()); // window.a is not a function
Même comportement que précédemment, la déclaration de la fonction a()
est déplacée par hoisting.
Le comportement est le même pour les arrow functions:
a = () => {
return 'from arrow function';
}
var a;
console.log(a()); // from arrow function
console.log(window.a()); // window.a is not a function
Les déclarations de fonctions sont déplacées avant les autres objets
Les déclarations des fonctions sont traitées prioritairement par rapport aux déclarations de variables.
Si on exécute le code suivant:
console.log(a()); // from a()
a = function() {
return 'from function expression';
}
var a;
function a() {
return 'from a()';
}
L’hoisting déplace le code de cette façon:
function a() {
return 'from a()';
}
console.log(a()); // from a()
a = function() {
return 'from function expression';
}
En résumé…
D’une façon générale, il faut éviter de déclarer des variables dans la portée globale en javascript. Pour que la portée d’une variable soit limitée, on utilise les mots-clé var
, let
ou const
:
var
est compatible avec toutes les versions de Javascript toutefois il entraîne certains comportements qui ne sont pas standards.let
est apparu à partir de ES6. La portée d’une variable déclarée avec ce mot-clé est standard par rapport aux autres langages.const
est similaire àlet
à la différence qu’il n’autorise pas de nouvelles affectations à une variable.
La portée d’une variable se limite à un bloc de code pour let
et const
. Avec var
, la portée correspond à une fonction, un bloc de code ne limite pas la portée.
Portée statique (i.e. lexical scope)
Le moteur Javascript utilise la portée statique pour déterminer la déclaration d’une variable. Suivant les différents mots-clé utilisés la notion de portée ne sera pas la même toutefois la recherche de la déclaration d’une variable se fait de la même façon:
- Lorsque le moteur d’exécution exécute la déclaration d’une variable, la portée de la variable sera celui dans lequel se trouve la déclaration.
- Lorsque le moteur d’exécution cherche la valeur d’une variable, il recherche sa déclaration dans la portée courante. Si aucune déclaration n’est trouvée, le moteur cherche dans les portées parentes. Si aucune déclaration n’est trouvée alors la variable appartient à la portée globale.
- Si une variable est déclarée dans une portée parente et qu’une variable est déclarée avec le même nom dans une portée enfant, la variable dans la portée parente sera occultée par celle de la portée enfant. Ce comportement correspond au shadowing.
eval() et new Function()
On peut modifier le comportement de la portée statique avec eval()
qui permet d’évaluer une expression sous forme de chaîne de caractères:
- La portée de l’expression évaluée par
eval()
est celui dans lequeleval()
est exécuté. L’expression peut être construite dans une portée différente, toutefois c’est au moment de l’exécution que la portée de l’expression sera confondue avec la portée dans laquelleeval()
est exécutée. - En mode strict, la portée de l’expression évaluée par
eval()
est limitée àeval()
, elle n’est pas confondue avec la portée dans laquelleeval()
est exécutée.
Le constructeur new Function()
permet aussi d’évaluer une expression sous forme de chaîne de caractères. La différence avec eval()
est que la portée parente d’un objet de type Function
est la portée globale et non la portée dans laquelle new Function()
est exécutée.
this
La valeur de this
dépend du contexte dans lequel il est utilisé:
- Dans une classe,
this
est l’instance de cette classe. - Dans un objet,
this
est l’instance de cet objet. - Dans une fonction n’appartenant pas à un objet:
- En mode non strict,
this
est le contexte global - En mode strict,
this
est indéfini.
- En mode non strict,
On peut affecter explicitement une valeur à this
quand on exécute une fonction en utilisant:
call()
:<fonction>.call(thisVar, [arg1, arg2,..., argN]);
apply()
:<fonction>.apply(thisVar, [tableau d'arguments]);
bind()
qui permet de créer une nouvelle référence vers une fonction:let <référence fonction> = <fonction>.bind(thisVar, [arg1, arg2,..., argN]);
Si on exécute une fonction avec setTimeout()
, par défaut, this
fait référence au contexte global.
Hoisting
- Si on déclare une variable avec
var
, le comportement d’hoisting du moteur Javascript peut entraîner un déplacement des lignes de code d’un script pour prendre en compte la déclaration d’une variable avant son initialisation. Même si dans le script initial, l’initialisation se trouve avant la déclaration, le moteur prendra en compte d’abord la déclaration. - Il n’y a pas d’hoisting si on utilise
let
ouconst
. - L’hoisting est valable aussi pour les fonctions anonymes ou les arrow functions.
- Les déclarations de fonctions sont déplacées avant les autres objets.
- Classes (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Classes
- Function.prototype.call() (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Function/call
- Function.prototype.bind() (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Function/bind
- Function.prototype.apply() (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Function/apply
- L’opérateur this (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Opérateurs/L_opérateur_this
- eval() (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/eval
- bloc (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Statements/block
- let (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Statements/let
- const (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Statements/const
- Function (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Function
- Closures (Fermetures) (MDN Web docs): https://developer.mozilla.org/fr/docs/Web/JavaScript/Closures
- JavaScript Anonymous Functions: https://www.javascripttutorial.net/javascript-anonymous-functions/
- You Don’t Know JS (Kyle Simpson): https://github.com/getify/You-Dont-Know-JS
- You Don’t Know JS (book series): https://maximdenisov.gitbooks.io/you-don-t-know-js/content/
- Static and Dynamic Scoping: https://www.geeksforgeeks.org/static-and-dynamic-scoping/
- ES6 – Variables: https://www.tutorialspoint.com/es6/es6_variables.htm
- Javascript — Lexical and Dynamic Scoping?: https://medium.com/@osmanakar_65575/javascript-lexical-and-dynamic-scoping-72c17e4476dd
- When to use a function declaration vs. a function expression: https://www.freecodecamp.org/news/when-to-use-a-function-declarations-vs-a-function-expression-70f15152a0a0/
- Of Function Scope and Lexical Scoping: http://pierrespring.com/2010/05/11/function-scope-and-lexical-scoping/
- What You Should Already Know about JavaScript Scope: https://spin.atomicobject.com/2014/10/20/javascript-scope-closures/
- Lexical scopes for function expressions (stackoverflow.com): https://stackoverflow.com/questions/36240571/lexical-scopes-for-functions-expressions
- What is the difference between a function expression vs declaration in JavaScript? (stackoverflow.com): https://stackoverflow.com/questions/1013385/what-is-the-difference-between-a-function-expression-vs-declaration-in-javascrip
- Static (Lexical) Scoping vs Dynamic Scoping (Pseudocode) (stackoverflow.com): https://stackoverflow.com/questions/22394089/static-lexical-scoping-vs-dynamic-scoping-pseudocode
- How to use typed variables in javascript? (stackoverflow.com): https://stackoverflow.com/questions/9659753/how-to-use-typed-variables-in-javascript
scope ne veut rien dire en français, on parle simplement de portée d’une variable
Ca peut paraître facile mais quand on rédige un article en français se pose souvent la question d’utiliser le terme français parfois peu utilisé ou de faire des anglicismes et de garder le terme en anglais. A la réflexion, effectivement utiliser le terme “portée” plutôt que “scope” semble plus approprié