ES3, Chap. 1 — Les contextes d'exécution en JavaScript

Ce billet fait partie de la collection ES3 dans le détail et en constitue le Chapitre 1.

Comme dans Inception, les contextes s'empilent les uns au-dessus des autres.
Comme dans Inception, les contextes s'empilent les uns au-dessus des autres.

Dans ce billet nous évoquerons les contextes d'exécution en JavaScript, ainsi que leurs différentes déclinaisons.

Définition

Chaque fois que du code JavaScript est exécuté, nous entrons dans un contexte d'exécution.

Le contexte d'exécution (dont la forme abrégée sera EC pour « execution context ») est un concept abstrait décrit par la spécification ECMA-262-3 pour classifier et différentier différents types de code exécutable.

Ce standard ne définit aucune structure ni aucune déclinaison en terme d'implémentation technique des contextes d'exécution. C'est un problème qui doit être traité par les moteurs qui implémentent le standard.

Pour résumer, un groupe de contexte d'exécution forme une pile (nommée « stack »). Le bas de cette pile est toujours le contexte global (« global context ») alors que le sommet est le contexte d'exécution courant (« active context »). La pile est augmentée (« pushed  ») lors de l'entrée dans un contexte d'exécution et diminuée (« popped  ») lors de sa sortie.

Les différentes déclinaisons de code exécutable

Le concept de déclinaison de code exécutable est directement lié au concept abstrait de contexte d'exécution. Il y a le code global, le code des fonctions et le code d'évaluation intégrée. Voyons cela plus en détails.

Nous pouvons définir la pile des contextes d'exécution comme un tableau :

Pseudo-code

ECStack = []

La pile est augmentée chaque fois que nous entrons dans une fonction (même si la fonction est appelée récursivement ou en tant que constructeur). Cela est également le cas lors de l'utilisation de la fonction d'évaluation intégrée eval.

Code global

Ce type de code est traité au niveau Programme : c.-à-d. en chargeant un fichier externe .js ou un code se trouvant dans des balises <script></script>. Aucune partie de code qui se trouve à l'intérieur d'une fonction n'est du code global. Le code activé ici fait parti du contexte global.

À l'initialisation (démarrage du programme), la pile ECStack ressemble à ceci :

Pseudo-code

ECStack = [
    globalContext
]

Code de fonction

En entrant dans un code de fonction (quel qu'il soit), la pile ECStack est augmentée avec un nouvel élément. Il est important de préciser qu'aucune partie de code définie dans une sous-fonction n'appartient à la fonction courante. Le code activé dans des fonctions forme un contexte de fonction.

Prenons l'exemple d'une fonction qui va s'appeler elle-même de manière récursive juste une seule fois :

Code JavaScript

(function dream(flag) {
    if (flag) {
        return;
    }
    dream(true);
})(false);

Et bien, la pile ECStack est modifiée comme suit :

Pseudo-code

// première activation de `dream`
ECStack = [
    <dream> functionContext,
    globalContext
]

// activation récursive de `dream`
ECStack = [
    <dream> functionContext (récursivement),
    <dream> functionContext,
    globalContext
]

Chaque return implicite ou explicite d'une fonction fait quitter le contexte d'exécution courant et diminue la pile ECStack en conséquence, refermant la pile de haut en bas. Une fois que l'exécution de ces codes est terminée, ECStack contient de nouveau uniquement le globalContext jusqu'à ce que le programme se termine.

Une exception lancée mais non interceptée peut aussi mettre fin à un ou plusieurs contextes d'exécution :

Code JavaScript

(function chase() {
    (function hotel() {
        (function fortress() {
            throw 'Sort des contextes de `fortress` puis `hotel` puis `chase`';
        })();
    })();
})();

Code d'évaluation intégrée

Les choses sont plus intéressantes avec du code d'évaluation intégrée. Dans ce cas, il y a un concept de contexte appelant (« calling context »), c.-à-d. un contexte depuis lequel la fonction eval est appelée.

Les actions réalisées par eval, comme la définition de variable ou de fonction, influence le contexte appelant :

Code JavaScript

// influence le contexte global
eval('var x = 10');

(function limbo() {
    // ici, la variable `y`
    // est créée dans le contexte local
    // de la fonction `limbo`
    eval('var y = 20');
})();

alert(x); // `10`
alert(y); // `y` n'est pas défini(e)

Notons qu'en mode strict à partir de ES5, eval n'influence plus le contexte appelant mais évalue son code dans un bac à sable local.

Code JavaScript

"use strict";

// influence le contexte global
eval('var x = 10');

alert(x); // `x` n'est pas défini(e)

Pour l'exemple ci-dessus, nous avons les modifications de la pile ECStack suivantes :

Pseudo-code

ECStack = [
    globalContext
]

// eval('var x = 10');
ECStack.pushContext({
    context: evalContext,
    callingContext: globalContext
})

// sortie du contexte `evalContext`
ECStack.popContext()

// appel de la fonction `limbo`
ECStack.pushContext(<limbo> functionContext);

// eval('var y = 20');
ECStack.pushContext({
    context: evalContext,
    callingContext: <limbo> functionContext
})

// sortie du contexte `evalContext`
ECStack.popContext()

// sortie de foo
ECStack.popContext()

Ceci est une représentation de la logique de la pile d'appels (« call-stack »).

Dans de vieilles implémentations de Mozilla Firefox, et ceux jusqu'à la version 1.7 de son moteur JavaScript (SpiderMonkey), il était possible de passer un contexte appelant en tant que second paramètre pour la fonction eval. Ainsi, quand le contexte existait toujours, il était possible d'en influencer les variables privées :

Code JavaScript

function reality() {
    var x = 1;
    return function () { alert(x); };
};

var dream = reality();

dream(); // `1`

eval('x = 2', dream); // contexte passé et qui influence la variable interne `x`

dream(); // `2`

Cependant, pour des raisons de sécurité, les moteurs modernes ne permettent plus cela désormais.

Code de module

Pour finir, notons qu'en ES6+ (ou ES2015+), il existe un quatrième type de code qui est le code de module.

Code global et implémentation dans les navigateurs

Nous avons vu plus haut que le code global s'exécutait au niveau Programme. Cela signifie que le code global est le code qui s'active dès l'entrée dans un script et qui crée le contexte global. Mais dans un navigateur ? Il y a « plusieurs » balises <script>. Font-elles toutes partie d'un même contexte global ?

En réalité il y a plusieurs contextes globaux exécutés dans un navigateur. Un pour chaque script JavaScript rencontré qu'il soit dans les balises <script> ou dans un attribut HTML demandant à être analysé en tant que JavaScript. Cependant, tous ces contextes globaux partage un seul et même objet global : plus de détails dans le prochain chapitre.

Notons qu'en ce qui concerne Node.js il n'y a qu'un seul contexte global par commande exécutée avec node et c'est celui du script appelé. Ainsi tous les appels par le mécanisme require/export à l'intérieur du script est un code de fonction.

Conclusion

C'est le minimum théorique requis pour analyser plus en profondeur des éléments liés au fonctionnement des contextes d'exécution comme l'objet des variables ou la chaîne des portées dont vous trouverez plus de détails dans les chapitres appropriés.

Références

Section correspondante de la spécification ECMA-262-3 — 10. Execution Contexts.

Ce texte est une libre ré-écriture française de l'excellent billet Тонкости ECMA-262-3. Часть 1. Контексты исполнения. de Dmitry Soshnikov.

Lire dans une autre langue