ES3, Chap 4. — La chaîne des portées en JavaScript
Ce billet fait partie de la collection ES3 dans le détail et en constitue le Chapitre 4.
Ce chapitre est dédié, encore une fois, à un mécanisme lié aux contextes d'exécution : j'ai nommé la chaîne des portées.
Introduction
Comme nous l'avons vu dans le deuxième chapitre, les données d'un contexte d'exécution (variables, déclarations de fonctions et paramètres formels) sont stockés dans des propriétés de l'objet des variables (dont la forme abrégée sera VO pour « variable object »).
Nous avons également vu qu'un objet des variables est créé et lié à chaque entrée dans un contexte d'exécution avec des valeurs initiales et que ces valeurs sont mises à jour pendant la phase d'exécution.
Voyons à présent ce qu'il en est pour la chaîne des portées.
Définition
La chaîne des portées est intimement liée aux fonctions internes.
Comme nous le savons, il est permis en JavaScript de créer des fonctions à l'intérieur d'autres fonctions et il est même permis de faire retourner cette fonction interne par la fonction principale.
Code JavaScript
var manuscripts = 20;
function desmond() {
var letters = 20;
function edward() {
alert(manuscripts + letters);
}
return edward;
}
desmond()(); // `40`
Nous savons également que chaque contexte d'exécution a son propre objet des variables :
- pour le contexte global c'est l'objet global (dont la forme abrégée sera GO pour « global object ») lui-même et
- pour les contextes de fonctions c'est un objet d'activation (dont la forme abrégée sera AO pour « activation object »).
Et où vient se placer la chaîne des portées dans tout cela ?
Eh bien la chaîne des portées est très exactement la liste de tous les objets des variables parents pour un contexte donné. C.-à-d., dans l'exemple précédent, que la chaîne des portées du contexte de edward inclut VO(<edward> functionContext), VO(<desmond> functionContext) et VO(globalContext) (ou écrit autrement : AO(<edward>), AO(<desmond>) et GO).
Examinons cela plus en détail.
La chaîne des portées (dont la forme abrégée sera Scope pour « scope chain ») est liée à la chaîne des objets des variables d'un contexte et est utilisée pour trouver en amont une variable, une fonction ou un paramètre lors de la résolution d'identifiant.
La chaîne des portées d'un contexte de fonction est créée lors de l'appel de celle-ci et est une propriété de l'objet d'activation. La chaîne des portées est résolue grâce à la propriété interne [[Scope]] appartenant à la fonction elle-même. Nous discuterons plus en détail de cette propriété [[Scope]] plus bas.
Concernant la chaîne des portées, elle peut-être décrite de la manière suivante avec du pseudo-code :
Pseudo-code
activeExecutionContext = {
AO: { <...> }, // ou `VO`
this: <valeur de this>,
Scope: [ // chaîne des portées
// liste de tous les objets des variables
// pour la résolution d'identifiant en amont
]
}
où Scope est par définition
Pseudo-code
Scope = AO + [[Scope]]
Nous pouvons représenter Scope et [[Scope]] comme un tableau standard en JavaScript :
Pseudo-code
// chaîne des objets des variables parents
[[Scope]] = [VO1, VO2, ..., VOn] // propriété de l'objet de la fonction
// chaîne des portées
Scope = AO + [[Scope]] // propriété du contexte d'exécution de la fonction
Ce qui nous donne pour notre exemple précédent dans le cas de la fonction edward :
Pseudo-code
edward.[[Scope]] = [AO(<desmond>), GO]
// ou encore
Scope(<edward> functionContext) = [AO(<edward>)].concat(edward.[[Scope]])
Une vue alternative de la structure serait de représenter cela comme une chaîne d'objets hiérarchiques avec une référence à l'objet des variables parents pour chaque maillon de la chaîne. C'est le même concept utilisant la fonctionnalité __parent__ implémentée dans certains moteurs JavaScript que nous avons vus dans le chapitre 2 dédié à l'objet des variables. Ce qui donnerait pour edward et desmond :
Pseudo-code
edward.[[Scope]] = {
letters: 20,
__parent__: desmond.[[Scope]]
}
desmond.[[Scope]] = {
manuscripts: 20,
__parent__: null
}
Mais la représentation de la chaîne des portées sous forme de tableau étant la plus pratique, nous utiliserons cette représentation. C'est d'ailleurs l'abstraction qu'en fait la spécification elle-même (voir 10.1.4) : « une chaîne de portées est une liste d'objets » indépendamment de ce qui sera fait au niveau de l'implémentation. Pour en revenir à notre article, le tableau est donc un bon candidat pour représenter ce concept de liste. Soit pour edward :
Pseudo-code
edward.[[Scope]] = [AO(<desmond>), GO]
c.-à-d.
Pseudo-code
edward.[[Scope]] = [{
letters: 20
}, {
manuscripts: 20
}]
La combinaison AO + [[Scope]] ainsi que le processus de résolution d'identifiant, dont nous allons parler plus loin, sont liés au cycle de vie des fonctions.
Cycle de vie des fonctions
Le cycle de vie d'une fonction est divisé en une phase de création de la fonction (avec function) et une phase d'activation de la fonction (appel de la fonction avec ()).
Création de la fonction
Comme nous le savons, les déclarations de fonctions (dont la forme abrégée sera FD pour « function declaration ») sont mises dans les objets des variables (objet global ou objet d'activation) lors de la phase d'entrée dans le contexte. Voyons dans l'exemple ci-dessous une variable et une déclaration de fonction dans le contexte global (où l'objet des variables est l'objet global lui-même — pas d'objet d'activation) :
Code JavaScript
var manuscripts = 20;
function altaïr() {
var letters = 20;
alert(manuscripts + letters);
}
altaïr(); // `40`
Lors de l'activation (l'appel) de la fonction altaïr, nous voyons le résultat 40. Il y a une fonctionnalité très importante qui se cache là-dessous.
La variable letters est définie dans la fonction altaïr (cela signifie qu'elle est incluse dans l'objet d'activation du contexte de altaïr), mais la variable manuscripts n'est pas définie dans altaïr et donc n'est pas ajoutée dans AO(<altaïr>). À première vue, la variable manuscripts n'existe pas du tout dans la fonction altaïr, mais comme nous allons le voir plus bas, seulement « à première vue ». Nous voyons dans l'objet d'activation du contexte de altaïr uniquement la propriété letters :
Pseudo-code
AO(<altaïr>) = {
letters: undefined // à l'entrée puis `20` lors de l'exécution
}
Comment la fonction altaïr a-t-elle accès à la variable manuscripts ? La logique voudrait que cette fonction ait accès aux objets des variables des contextes plus em amont de la pile (« stack »). C'est exactement le cas, et ce mécanisme est implémenté en utilisant la propriété interne [[Scope]] des fonctions.
[[Scope]] est une chaîne hiérarchique contenant tous les objets des variables parents qui sont avant dans la pile des contextes d'exécution et cette chaîne est ajoutée dans la fonction à sa création.
Notons ce point important : [[Scope]] est ajoutée lors de la création de manière statique une seule fois jusqu'à ce que celle-ci soit détruite. Une fonction peut ne jamais être appelée (activée), mais la propriété [[Scope]] est déjà écrite et stockée dans la fonction.
Prenons maintenant un moment pour considérer notre propriété [[Scope]] qui contrairement à la propriété Scope (la chaîne des portées) est une propriété inaccessible de la fonction elle-même et non du contexte :
Pseudo-code
altaïr.[[Scope]] = [
VO(globalContext) // === GO
]
Par la suite, lors de l'appel de la fonction, nous entrons dans le contexte d'exécution de celle-ci. Alors l'objet d'activation est créé puis this et Scope (la chaîne des portées) sont déterminés.
Activation de la fonction
Comme dit dans la définition, lors de la phase d'entrée dans le contexte, après la création de l'objet d'activation (c.-à-d. l'objet des variables), la propriété Scope du contexte d'exécution (qui est la chaîne des portées pour trouver en amont les variables) est définie comme suit :
Pseudo-code
Scope = AO + [[Scope]]
On voit ici que l'objet d'activation est le premier élément du tableau de Scope, c.-à-d. qu'il est ajouté en amont de la chaîne des portées :
Pseudo-code
Scope = [AO].concat([[Scope]])
Cette fonctionnalité est très importante pour la résolution d'identifiant.
La résolution d'identifiant est un processus qui détermine à quel objet des variables, dans la chaîne des portées, une variable (ou une déclaration de fonction) appartient.
En retour de cet algorithme nous avons toujours une valeur de type Reference dont la base est l'objet des variables correspondant (ou null si la variable n'est pas trouvée) et où le nom de propriété est le nom de l'identifiant trouvé. Le type Reference est discuté plus en détail dans le chapitre 3.
Le processus de résolution d'identifiant inclut une vérification en amont des propriétés correspondantes au nom de la variable, c.-à-d. qu'il y a une examination consécutive de chaque objet des variables à travers la chaîne des portées en commençant par le contexte du sommet de la pile en descendant jusqu'au plus profond (en commençant donc par AO (ou directement VO dans le contexte global) puis en continuant dans [[Scope]]).
Ainsi, les variables locales du contexte d'exécution ont une plus haute priorité que les variables en provenance des contextes d'exécution parents. Dans le cas où deux variables de même nom (mais de contextes différents) existent, c'est la première valeur trouvée (celle la plus haute dans la pile) qui est utilisée pour la résolution.
Compliquons un peu l'exemple précédent et ajoutons de nouveaux niveaux de fonctions internes :
Code JavaScript
var manuscripts = 20;
function desmond() {
var letters = 20;
function edward() {
var songs = 24;
alert(manuscripts + letters + songs);
}
edward();
}
desmond(); // `64`
Pour cet exemple voici ci-dessous les objets des variables (objet global et objets d'activation) associés ainsi que les propriétés [[Scope]] des fonctions et leur chaîne des portées des contextes lors de la phase d'exécution :
L'objet des variables du contexte global est :
Pseudo-code
VO(globalContext) === GO = {
manuscripts: 10
desmond: <référence à la `FD`>
}
Lors de la création de desmond, la propriété [[Scope]] de desmond est :
Pseudo-code
desmond.[[Scope]] = [
GO
]
Lors de l'appel de la fonction desmond, l'objet d'activation du contexte de desmond est :
Pseudo-code
VO(<desmond> functionContext) === AO(<desmond>) = {
letters: 20,
edward: <référence à la `FD`>
}
Et la chaîne des portées du contexte de desmond est :
Pseudo-code
Scope(<desmond> functionContext) = AO(<desmond>) + desmond.[[Scope]]
c.-à-d.
Pseudo-code
Scope(<desmond> functionContext) = [
AO(<desmond>),
GO
]
Lors de la création de la fonction interne edward, la propriété [[Scope]] de edward est :
Pseudo-code
edward.[[Scope]] = [
AO(<desmond>),
GO
]
Lors de l'appel de la fonction edward, l'objet d'activation du contexte de edward est :
Pseudo-code
VO(<edward> functionContext) === AO(<edward>) = {
songs: 24
}
Et la chaîne des portées du contexte de edward est :
Pseudo-code
Scope(<edward> functionContext) = AO(<edward>) + edward.[[Scope]]
c.-à-d.
Pseudo-code
Scope(<edward> functionContext) = [
AO(<edward>),
AO(<desmond>),
GO
]
Et les résolutions d'identifiants pour les noms manuscripts, letters et songs se font comme suit :
Schéma
`manuscripts`
└─ AO(<edward>) // pas trouvé
└─ AO(<desmond>) // pas trouvé
└─ GO // `20` trouvé
Pseudo-code
`letters`
└─ AO(<edward>) // pas trouvé
└─ AO(<desmond>) `20` trouvé
Pseudo-code
`songs`
└─ AO(<edward>) // `24` trouvé
Les fonctionnalités des portées
Entrons maintenant plus en détail dans des fonctionnalités importantes liées aux chaînes des portées et à la propriété [[Scope]] des fonctions.
Fermetures
Les fermetures (« closures ») en JavaScript ont un rapport direct avec la propriété [[Scope]] des fonctions. Comme nous l'avons déjà vu, [[Scope]] est ajoutée lors de la création de la fonction et existe jusqu'à ce qu'elle soit détruite. En fait, une fermeture est la combinaison entre le code d'une fonction et sa propriété [[Scope]]. Ainsi [[Scope]] contient l'environnement lexical (les objets des variables des parents) quand sa fonction est créée. Les variables venant des contextes d'exécution plus bas dans la pile lors de l'activation (appel) des fonctions vont être cherchées dans la chaîne des portées (statiquement ajoutée lors de la création).
Exemples :
Code JavaScript
var almanacs = 20;
function connor() {
alert(almanacs);
}
(function () {
var almanacs = 16;
connor(); // `20`, mais pas `16`
})();
Nous voyons que la variable almanacs est trouvée dans le [[Scope]] de la fonction connor. Souvenons-nous que pour la résolution de variables, nous utilisons la chaîne des portées qui a été créée au moment de la création de la fonction, et qui n'est pas dynamiquement mise à jour lors de l'appel (sinon la valeur almanacs aurait été résolue à 16).
Un autre exemple classique des fermetures :
Code JavaScript
function connor() {
var almanacs = 20;
var feathers = 100;
return function () {
alert([almanacs, feathers]);
};
}
var almanacs = 36;
var ratonhnhakéton = connor(); // `function () { alert([almanacs, feathers]); }`
ratonhnhakéton(); // `[20, 100]`
Encore une fois, nous voyons que la résolution des identifiants dans la chaîne des portées (définie lors de la création) est utilisée — la variable almanacs est résolue à 20 mais pas à 36. De plus, nous voyons clairement que la propriété [[Scope]] de la fonction (dans ce cas celui d'une fonction anonyme retournée par la fonction connor) continue d'exister même si le contexte depuis lequel la fonction a été créée est déjà terminé.
Plus de détails à propos du concept des fermetures dans les implémentations JavaScript seront donnés dans le chapitre 6.
[[Scope]] des fonctions créées par le constructeur Function
À la création d'une fonction, la propriété interne [[Scope]] est ajoutée et via cette propriété, nous avons accès aux variables de tous les contextes parents. Cependant, il y a une exception importante à cette règle et elle concerne les fonctions créées via l'objet Function.
Code JavaScript
var manuscripts = 20;
function desmond() {
var feathers = 100;
function connor() { // déclaration de fonction
alert(manuscripts);
alert(feathers);
}
var edward = function () { // expression de fonction
alert(manuscripts);
alert(feathers);
};
var ezio = Function('alert(manuscripts); alert(feathers);');
connor(); // `20`, `100`
edward(); // `20`, `100`
ezio(); // `20`, « erreur : `feathers` n'est pas définie »
}
desmond();
Comme nous pouvons le voir avec la fonction ezio, qui est créée via le constructeur Function, feathers n'est pas accessible. Mais cela ne veut pas dire que la fonction ezio n'a pas de propriété [[Scope]] interne (sinon elle n'aurait pas accès à manuscripts). Le point ici c'est que la propriété [[Scope]] d'une fonction créée via le constructeur Function contient uniquement l'objet global. Considérez donc que créer une fermeture de contexte parent (sauf pour le contexte global) via Function n'est pas possible.
Identification bidirectionnelle en amont dans la chaîne des portées
Il y a un autre point important pour la résolution d'identifiant en amont via la chaîne des portées. Ce sont les prototypes (s'il y en a) des objets des variables. Voici ce qu'on peut dire sur la nature prototypale du JavaScript : si une propriété n'est pas trouvée directement dans l'objet, la résolution d'identifiant en amont se fait dans la chaîne des prototypes. C.-à-d. qu'il y a une recherche bidirectionnelle : (1) à travers la chaîne des portées, (2) et sur chaque portée de la chaîne, à travers la chaîne des prototypes. Nous pouvons observer cet effet si nous définissons une propriété dans Object.prototype :
Code JavaScript
function altaïr() {
alert(creed);
}
Object.prototype.creed = 'Nous œuvrons dans les ténèbres pour servir la lumière.';
altaïr(); // `Nous œuvrons dans les ténèbres pour servir la lumière.`
Les objets d'activation n'ont pas de prototype comme nous pouvons le voir dans l'exemple suivant :
Code JavaScript
function connor() {
var creed = 'Rien n'est vrai';
function edward() {
alert(creed);
}
edward();
}
Object.prototype.creed = 'Tout est permis';
connor(); // `'Rien n'est vrai'`
Si l'objet d'activation du contexte de la fonction edward avait un prototype, alors la propriété creed aurait été trouvée dans Object.prototype, car elle n'est pas trouvée directement dans l'objet d'activation. Par contre, dans le premier exemple au-dessus, en traversant la chaîne des portées pour la résolution d'identifiant, nous trouvons creed car l'objet global (dans beaucoup d'implémentations mais pas dans toutes) hérite bien de l'objet Object.prototype et, par conséquent, creed est résolue avec Nous œuvrons dans les ténèbres pour servir la lumière..
Une situation similaire peut être observée dans plusieurs versions de Mozilla Firefox (SpiderMonkey) avec les expressions de fonctions nommées, où un objet spécial d'activation qui enregistre le nom optionnel des expressions de fonction nommées hérite de Object.prototype. Mais ces fonctionnalités seront vues plus en détail dans le chapitre 5.
Chaîne des portées des contextes global et de eval
Ici il n'y a rien de bien intéressant, mais il est nécessaire de le préciser. Il y a aussi une chaîne des portées pour le contexte global, mais elle contient seulement l'objet global. Les contextes de eval on la même chaîne des portées que le contexte appelant.
Pseudo-code
Scope(globalContext) = [
GO
]
et
Pseudo-code
Scope(evalContext) === Scope(callingContext)
Affectation de la chaîne des portées lors de l'exécution du code
En JavaScript il y a deux instructions qui peuvent modifier la chaîne des portées pendant la phase d'exécution du code. Ce sont les structures de contrôle with et catch. Toutes les deux ajoutent en amont de la chaîne des portées l'objet nécessaire à la résolution des identifiants apparaissant à l'intérieur de ces instructions. C.-à-d. que si l'un de ces deux cas de figure intervient, la chaîne des portées est augmentée ainsi :
Pseudo-code
Scope = __withObject + VO + [[Scope]]
ou
Pseudo-code
Scope = __catchObject + VO + [[Scope]]
La structure de contrôle with, dans ce cas, ajoute l'objet qui est en paramètre (et les propriétés de cet objet deviennent accessibles sans préfixe) :
Code JavaScript
var collectibles = { letters: 20, almanacs: 36 };
with (collectibles) {
alert(letters); // `20`
alert(almanacs); // `36`
}
c.-à-d.
Pseudo-code
Scope = collectibles + VO + [[Scope]]
Montrons une nouvelle fois comment la résolution d'identifiant s'effectue dans l'objet ajouté par la structure de contrôle with en amont de la chaîne des portées :
Code JavaScript
var assassins = 10, templars = 10;
with ({ assassins: 20 }) {
var assassins = 30, templars = 30;
alert(assassins); // `30`
alert(templars); // `30`
}
alert(assassins); // `10`
alert(templars); // `30`
Qu'est-ce qu'il s'est passé ici ? Lors de la phase d'entrée dans le contexte, les identifiants assassins et templars sont ajoutés dans l'objet des variables. Plus tard, elles sont déjà présentes lors de la phase d'exécution du code, et les modifications suivantes sont faites :
- assassins = 10, templars = 10,
- l'objet { assassins: 20 } est ajouté en amont de la chaîne des portées,
- la rencontre du mot-clé var dans with ne fait rien car toutes les variables ont été évaluée lors de la phase d'entrée dans le contexte,
- il n'y a que la modification de la valeur de assassins qui intervient, et assassins sera trouvée maintenant dans l'objet ajouté en amont de la chaîne des portées à l'étape 2. La valeur de assassins était 20 et devient 30,
- il y a aussi une modification de templars qui est résolue depuis l'objet des variables initial et donc, qui de 10 devient 30,
- quand la structure de contrôle with a fini d'être exécutée, son objet spécial est retiré de la chaîne des portées (et la valeur modifiée assassins est supprimée avec cet objet). C.-à-d. que la structure initiale de la chaîne des portées est restaurée à son état initial d'avant l'augmentation de with,
- et comme nous pouvons le voir dans les deux dernières alertes : la valeur de assassins de l'objet des variables courant reste le même et la valeur de templars est maintenant égale à 30 telle qu'elle a été changée dans la structure de contrôle with.
C'est la même chose avec la structure de contrôle catch qui, dans le but d'avoir accès au paramètre d'exception, crée un objet de portée intermédiaire avec une unique propriété — le nom du paramètre d'exception. Cet objet est placé en amont de la chaîne des portées. Ce qui donne
Code JavaScript
try {
/* ... */
} catch (creed) {
alert(creed);
}
la modification de chaîne des portées suivante :
Pseudo-code
catchObject = {
creed: <objet de l'exeption>
}
Scope = __catchObject + VO + [[Scope]]
Quand le travail de la structure de contrôle catch est fini, la chaîne des portées est aussi restaurée à son état premier.
Conclusion
À ce niveau, nous avons vu tous les concepts généraux concernant les contextes d'exécution et les détails associés. Nous allons maintenant entrer dans une analyse détaillée des fonctions comme leur type (déclaration de fonction ou expression de fonction) et les fermetures.
Références
Section correspondante de la spécification ECMA-262-3 :
- [8.6.2 – [[Scope]]](https://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%203rd%20edition,%20December%201999.pdf),
- 10.1.4 – Scope Chain and Identifier Resolution.
Ce texte est une libre adaptation française de l'excellent billet Тонкости ECMA-262-3. Часть 4. Цепь областей видимости. de Dmitry Soshnikov.