ES3 en détails

Tutoriel pour apprendre la chaîne des portées en JavaScript

Bruno Lesieur

Ce tutoriel est dédié, encore une fois, à un mécanisme lié aux contextes d'exécution : j'ai nommé la chaîne des portées.

Commentez

- Titre : ES3 en détails

- Auteur : Bruno Lesieur

- Parution : 28 février 2018

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2018 Nom Auteur. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- ES3 en détails - les contextes d'exécution en JavaScript

- ES3 en détails - l'objet des variables en JavaScript

- ES3 en détails - la valeur de this en JavaScript

Sommaire

- I. Introduction

- II. Définition

- III. Cycle de vie des fonctions

- III-A. Création de la fonction

- III-B. Activation de la fonction

- IV. Les fonctionnalités des portées

- IV-A. Fermetures

- IV-B. [[Scope]] des fonctions créées par le constructeur Function

- IV-C. Identification bidirectionnelle en amont dans la chaîne des portées

- IV-D. Chaîne des portées des contextes global et de eval

- IV-E. Affectation de la chaîne des portées lors de l'exécution du code

- V. Conclusion

- VI. Remerciements

I - 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.

II - 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 éventuellement permis de faire retourner cette fonction interne par la fonction principale.

1. var manuscripts = 20;

3. function desmond() {

5. var letters = 20;

7. function edward() {

8. alert(manuscripts + letters);

9. }

11. return edward;

13. }

15. 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  ;

- 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'est-à-dire, 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é [[Scope]] appartenant à la fonction elle-même. Nous discuterons plus en détail de cette propriété interne [[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

1. activeFunctionContext = {

2. AO: {

3. <...>, // données du contexte `VD`, `FD` et `FP`,

4. arguments: <ArgO>

5. },

6. this: <valeur de this>,

7. Scope: [ // chaîne des portées

8. // liste de tous les objets des variables

9. // pour la résolution d'identifiant en amont

10. ]

11. }

ou Scope est par définition

Pseudo-code

1. Scope = AO + [[Scope]]

Nous pouvons représenter Scope et [[Scope]] comme un tableau standard en JavaScript :

Pseudo-code

1. // chaîne des objets des variables parents

2. [[Scope]] = [VO1, VO2, ..., VOn] // propriété de l'objet de la fonction

4. // chaîne des portées

5. 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

1. edward.[[Scope]] = [AO(<desmond>), GO]

2. 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

1. edward.[[Scope]] = {

2. letters: 20,

3. __parent__: desmond.[[Scope]]

4. }

6. desmond.[[Scope]] = {

7. manuscripts: 20,

8. __parent__: null

9. }

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

1. edward.[[Scope]] = [AO(<desmond>), GO]

C'est-à-dire

Pseudo-code

1. edward.[[Scope]] = [{

2. letters: 20

3. }, {

4. manuscripts: 20

5. }]

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.

III - 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 ()).

III-A - 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) :

1. var manuscripts = 20;

3. function altaïr() {

4. var letters = 20;

5. alert(manuscripts + letters);

6. }

8. altaïr(); // `40`

Lors de l'activation (l'appel) de la fonction foo, 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

1. AO(<altaïr>) = {

2. letters: undefined // à l'entrée puis `20` lors de l'exécution

3. }

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 bas dans 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

1. altaïr.[[Scope]] = [

2. VO(globalContext) // === GO

3. ]

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.

III-B - 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'est-à-dire 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

1. Scope = AO + [[Scope]]

On voit ici que l'objet d'activation est le premier élément du tableau de Scope, c'est-à-dire. qu'il est ajouté en amont de la chaîne des portées :

Pseudo-code

1. 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'est-à-dire 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 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 :

1. var manuscripts = 20;

3. function desmond() {

5. var letters = 20;

7. function edward() {

8. var songs = 24;

9. alert(manuscripts + letters + songs);

10. }

12. edward();

13. }

15. 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

1. VO(globalContext) === GO = {

2. manuscripts: 10

3. desmond: <référence à la `FD`>

4. }

Lors de la création de desmond, la propriété [[Scope]] de desmond est :

Pseudo-code

1. desmond.[[Scope]] = [

2. GO

3. ]

Lors de l'appel de la fonction desmond, l'objet d'activation du contexte de desmond est :

Pseudo-code

1. VO(<desmond> functionContext) === AO(<desmond>) = {

2. letters: 20,

3. edward: <référence à la `FD`>

4. }

Et la chaîne des portées du contexte de desmond est :

Pseudo-code

1. Scope(<desmond> functionContext) = AO(<desmond>) + desmond.[[Scope]]

c'est-à-dire :

Pseudo-code

1. Scope(<desmond> functionContext) = [

2. AO(<desmond>),

3. GO

4. ]

Lors de la création de la fonction interne edward, la propriété [[Scope]] de edward est :

Pseudo-code

1. edward.[[Scope]] = [

2. AO(<desmond>),

3. GO

4. ]

Lors de l'appel de la fonction edward, l'objet d'activation du contexte de edward est :

Pseudo-code

1. VO(<edward> functionContext) === AO(<edward>) = {

2. songs: 24

3. }

Et la chaîne des portées du contexte de edward est :

Pseudo-code

1. Scope(<edward> functionContext) = AO(<edward>) + edward.[[Scope]]

c'est-à-dire

Pseudo-code

1. Scope(<edward> functionContext) = [

2. AO(<edward>),

3. AO(<desmond>),

4. GO

5. ]

Et les résolutions d'identifiants pour les noms manuscripts, letters et songs se font comme suit :

Schéma

1. `manuscripts`

2. └─ AO(<edward>) // pas trouvé

3. └─ AO(<desmond>) // pas trouvé

4. └─ GO // `20` trouvé

Pseudo-code

1. `letters`

2. └─ AO(<edward>) // pas trouvé

3. └─ AO(<desmond>) `20` trouvé

Pseudo-code

1. `y`

2. └─ AO(<edward>) // `24` trouvé

IV - 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.

IV-A - 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).

Exemple :

1. var almanacs = 20;

3. function connor() {

4. alert(almanacs);

5. }

7. (function () {

8. var almanacs = 16;

9. connor(); // `20`, mais pas `16`

10. })();

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 :

1. function connor() {

3. var almanacs = 20;

4. var feathers = 100;

6. return function () {

7. alert([almanacs, feathers]);

8. };

10. }

12. var almanacs = 36;

14. var ratonhnhakéton = connor(); // `function () { alert([almanacs, feathers]); }`

16. 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 un autre tutoriel.

IV-B - [[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.

1. var manuscripts = 20;

3. function desmond() {

5. var feathers = 100;

7. function connor() { // déclaration de fonction

8. alert(manuscripts);

9. alert(feathers);

10. }

12. var edward = function () { // expression de fonction

13. alert(manuscripts);

14. alert(feathers);

15. };

17. var ezio = Function('alert(manuscripts); alert(feathers);');

19. connor(); // `20`, `100`

20. edward(); // `20`, `100`

21. ezio(); // `20`, « erreur : `feathers` n'est pas définie »

23. }

25. 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.

IV-C - 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'est-à-dire 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 :

1. function altaïr() {

2. alert(creed);

3. }

5. Object.prototype.creed = 'Nous œuvrons dans les ténèbres pour servir la lumière.';

7. 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 :

1. function connor() {

3. var creed = 'Rien n\'est vrai';

5. function edward() {

6. alert(creed);

7. }

9. edward();

10. }

12. Object.prototype.creed = 'Tout est permis';

14. 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 prochain tutoriel de la série.

IV-D - 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 ont la même chaîne des portées que le contexte appelant.

Pseudo-code

1. Scope(globalContext) = [

2. GO

3. ]

et

Pseudo-code

1. Scope(evalContext) === Scope(callingContext)

IV-E - 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'est-à-dire. que si l'un de ces deux cas de figure intervient, la chaîne des portées est augmentée ainsi :

Pseudo-code

1. Scope = __withObject + VO + [[Scope]]

ou

Pseudo-code

1. 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) :

1. var collectibles = { letters: 20, almanacs: 36 };

3. with (collectibles) {

4. alert(letters); // `20`

5. alert(almanacs); // `36`

6. }

c'est-à-dire

Pseudo-code

1. 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 :

1. var assassins = 10, templars = 10;

3. with ({ assassins: 20 }) {

5. var assassins = 30, templars = 30;

7. alert(assassins); // `30`

8. alert(templars); // `30`

9. }

11. alert(assassins); // `10`

12. 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 :

1. assassins = 10, templars = 10 ;

2. L'objet { assassins: 20 } est ajouté en amont de la chaîne des portées ;

3. La rencontre du mot-clé var dans with ne fait rien, car toutes les variables ont été évaluées lors de la phase d'entrée dans le contexte ;

4. 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 ;

5. 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 ;

6. 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'est-à-dire. que la structure initiale de la chaîne des portées est restaurée à son état initial d'avant l'augmentation de with ;

7. 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

1. try {

2. /* ... */

3. } catch (creed) {

4. alert(creed);

5. }

la modification de chaîne des portées suivante :

Pseudo-code

1. catchObject = {

2. creed: <objet de l'exception>

3. }

5. 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.

V - Conclusion

Dans ce tutoriel, 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.

VI - Remerciements

Nous remercions Bruno Lesieur qui nous a autorisés à publier ce tutoriel.

Nous remercions également Laethy pour la mise au gabarit et Claude Leloup pour la correction orthographique.