ES3, Chap. 2 — L'objet des variables en JavaScript

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

Les Précogs sont aussi capable de voir à l'avance les variables pour la phase d'exécution !
Les Précogs sont aussi capable de voir à l'avance les variables pour la phase d'exécution !

Nous déclarons des variables et des fonctions avec lesquels tournent nos programmes. Mais comment, et quand l'interpréteur trouve ces données ? Que se passe t-il quand une référence à un objet est demandée ?

Introduction

Beaucoup de développeurs JavaScript savent que les variables sont intimement liées au contexte d'exécution :

Code JavaScript

var reportA = 10; // variable du contexte global

(function () {
    var reportB = 20; // variable locale d'un contexte de fonction
})();

alert(reportA); // 10
alert(reportB); // `reportB` n'est pas défini(e)

Beaucoup de développeurs savent également que la portée des variables est définie et limitée à l'exécution des contextes de fonctions. C.-à-d. que contrairement au C/C++, pour la structure de contrôle de boucle for par exemple, aucun contexte local n'est créé en JavaScript :

Code JavaScript

for (var report in { reportA: 1, reportB: 2 }) {
    alert(report);
}

alert(report); // la variable `report` est toujours dans la portée même si la boucle est terminée

Regardons plus en détail ce qu'il se passe quand nous déclarons nos données.

Déclaration de données

Si les variables sont liées à leur contexte d'exécution, celui-ci doit savoir où leurs données sont stockées et comment y accéder. Le mécanisme permettant cela est appelé l'objet des variables.

L'objet des variables (dont la forme abrégée sera VO pour « variable object ») est un objet spécial lié à un contexte d'exécution et qui stocke :

  • les déclarations de variables (dont la forme abrégée sera VD pour « variable declaration »),
  • les déclarations de fonctions (dont la forme abrégée sera FD pour « function declaration »)
  • les paramètres formels de fonctions (dont la forme abrégée sera FP pour « formal parameters »)

déclarés dans un contexte.

À noter qu'en ES5 le concept d'objet des variables est remplacé par le modèle des environnements lexicaux qui seront plus détaillés dans le chapitre approprié.

De façon schématique dans ces exemples, il est possible de présenter l'objet des variables comme un objet JavaScript standard :

Pseudo-code

VO = {}

Et comme nous l'avons dit, l'objet des variables est une propriété d'un contexte d'exécution :

Pseudo-code

activeExecutionContext = {
    VO: {
        <...> // données du contexte `VD`, `FD` (et `FP`)
    }
}

Il est possible d'avoir accès à l'objet des variables du contexte global par l'intermédiaire de l'objet global (car l'objet global est lui-même l'objet des variables dans ce cas). Pour tous les autres contextes, faire référence à l'objet des variables est impossible, c'est un mécanisme du moteur.

Quand nous déclarons une variable ou une fonction dans le contexte global, ce n'est rien d'autre que la création d'une nouvelle propriété au sein de l'objet des variables avec le nom et la valeur de notre variable (ou fonction).

Exemple :

Code JavaScript

var reportA = 10;

function precogs(minorityReport) {
    var reportB = 20;
};

precogs(30);

Avec les objets des variables correspondant :

Pseudo-code

// Objet des variables du contexte global
VO(globalContext) = {
    reportA: 10,
    precogs: <référence à la `FD` `precogs`>
}

// Objet des variables du contexte de la fonction `precogs`
VO(<precogs> functionContext) = {
    minorityReport: 30,
    reportB: 20
}

Voilà pour une vision théorique. Il faut juste être conscient qu'au niveau de l'implémentation réelle, l'objet des variables est quelque chose d'abstrait. Dans un contexte d'exécution réel, VO porte un autre nom et à une structure probablement différente.

L'objet des variables dans différents contextes d'exécution

Certaines opérations (comme l'affectation des variables) et comportement de l'objet des variables sont identiques pour toutes les déclinaisons de contexte d'exécution.

Pour expliquer cela, il va être nécessaire de présenter une partie de l'objet des variables comme commun à toutes les déclinaisons et une partie définissant des éléments et comportements additionnels.

Schéma

VO // contient des `VD` et des `FD`
║
╠══> VO(globalContext) === GO === this
║
╚══> VO(functionContext) === AO // contient des `FP` et l'`ArgO`

Voyons cela plus en détail.

L'objet des variables dans le contexte global

Tout d'abord il est nécessaire de donner une définition de l'objet global.

L'objet global (dont la forme abrégée sera GO pour « global object ») est un objet qui est déjà existant avant l'entrée dans n'importe quel contexte d'exécution car il est créé au démarrage du programme. Cet objet existe en un unique exemplaire, et ses propriétés sont accessibles depuis n'importe quel endroit du programme. Le cycle de vie de l'objet global s'arrête quand le programme se termine.

Notons que dans un environnement navigateur hôte, chaque script (utilisé avec la balise <script> ou dans un attribut à JavaScript) a son propre contexte d'exécution global. Cela signifie que pour une page web donnée, il existe « des » contexte d'exécution globaux. Cependant, comme expliqué juste au dessus, l'objet global est déjà existant dès qu'un script est exécuté et tous les contextes d'exécution globaux utilise un même objet global unique. Cependant, dans ce cas, le cycle de vie de l'objet global s'arrête seulement quand tous les programmes sont terminés. Nous verrons cela plus en détail plus loin.

À sa création, l'objet global est initialisé avec des propriétés comme Math, String, Date, parseInt, etc. Il possède parmi toutes ses propriétés une propriété circulaire faisant directement référence à lui-même.

Dans le model objet des navigateurs, cette propriété se nomme window :

Pseudo-code

activeGlobalContext = {
    GO: {
        Math: <...>,
        String: <...>,
        document: <...>,
        <...>,
        window: GO
    }
}

ou encore dans Node.js, elle se nomme global :

Pseudo-code

activeGlobalContext = {
    GO: {
        Math: <...>,
        String: <...>,
        process: <...>,
        <...>,
        global: GO
    }
}

Cela dépend de l'implémentation.

Quand nous faisons référence à une propriété de l'objet global nous n'utilisons aucun préfixe, juste son nom, car l'objet global n'est pas accessible. Cependant, il est possible d'en récupérer le contenu à travers sa référence circulaire à lui-même (window ou global) ou même à l'aide de la valeur this dans le contexte global. Voyons cela :

Code JavaScript

// sans préfixe
String(10); // === `GO.String(10)` mais `GO` n'est pas accessible

// avec préfixes
window.a = 10; // === `GO.window.a = 10` === `GO.a = 10`
this.b = 20; // === `GO.b = 20`

Nous avons dit plus haut que l'objet des variables dans le contexte global est l'objet global lui-même :

Pseudo-code

VO(globalContext) === GO

Il est nécessaire de bien comprendre cela car c'est pour cette raison qu'il est possible d'accéder à une variable déclarée avec le mot clé var en accédant à une propriété de l'objet global :

Code JavaScript

var mr = new String('test');

alert(mr); // accès direct, elle est trouvée dans `VO(globalContext)['mr']` => `'test'`

alert(window['mr']); // accès indirect via `GO.window['mr']` === `GO['mr']` === `VO(globalContext)['mr']` => `'test'`
alert(mr === this.mr); // `true`

var mrKey = 'mr';
alert(window[mrKey]); // accès indirect avec la résolution d'identifiant dynamique de propriété => `'test'`

L'objet global ne possède que des déclarations de variables et des déclarations de fonctions mais pas de paramètres formels.

L'objet des variables dans un contexte de fonction

En ce qui concerne les contextes d'exécution de fonctions l'objet des variables est inaccessible ni directement, ni indirectement. Il existe une équivalence de l'objet global pour le contexte de fonction qui est l'objet d'activation (dont la forme abrégée sera AO pour « activation object »).

Pseudo-code

VO(functionContext) === AO

Un objet d'activation est créé en entrant dans le contexte d'une fonction et initialisé avec la propriété arguments dont la valeur est l'objet des arguments (dont la forme abrégée sera ArgO pour « arguments object ») :

Pseudo-code

activeFunctionContext = {
    AO: {
        <...>, // données du contexte `VD`, `FD` et `FP`
        arguments: <ArgO>
    }
}

L'objet des arguments est une propriété de l'objet d'activation. Il contient les propriétés suivantes :

  • callee — la référence à la fonction courante,
  • length — la quantité d'arguments réellement passés,
  • [array-index] — des nombres (convertis en chaîne de caractère) dont les valeurs sont les mêmes que celles des paramètres formels (de gauche à droite dans la liste des paramètres). La taille de [array-index] est celle de arguments.length. Les valeurs de [array-index] (arguments réellement passés) de l'objet des arguments et celle des paramètres formels (paramètres définis) sont partagées.

Exemple :

Code JavaScript

function precogs(xReport, yReport, zReport) {

  // quantité de paramètres définis : xReport, yReport et zReport
  alert(precogs.length); // `3`

  // quantité d'arguments réellement passés : seulement xReport et yReport
  alert(arguments.length); // `2`

  // référence de la fonction à elle-même
  alert(arguments.callee === precogs); // `true`

  // paramètres partagés

  alert(xReport === arguments[0]); // `true`
  alert(xReport); // 10

  arguments[0] = 20;
  alert(xReport); // `20`

  xReport = 30;
  alert(arguments[0]); // `30`

  // cependant, pour l'argument non passé zReport,
  // le array-index de l'objet
  // des arguments n'est pas partagée

  zReport = 40;
  alert(arguments[2]); // `undefined`

  arguments[2] = 50;
  alert(zReport); // `40`

}

precogs(10, 20);

En ce qui concerne le dernier cas, dans les anciennes versions de Google Chrome (moteur V8) il y avait un bug — où les paramètres zReport et arguments[2] étaient aussi partagés.

En ES5 le concept d'objet d'activation est également remplacé par l'unique concept des environnements lexicaux.

Phases de traitement du code des contextes d'exécution

Après quelques mises au point, nous avons enfin atteint le sujet principal de cet article : Le traitement du code des contextes d'exécution. Ce traitement est divisé en deux phases :

  1. Entrée dans le contexte d'exécution ;
  2. Exécution du code.

Les modifications de l'objet des variables sont intimement liées à ces deux phases.

Entrée dans le contexte d'exécution

En entrant dans le contexte d'exécution (mais avant l'exécution du code), l'objet des variables est rempli avec les propriétés suivantes (elles ont déjà été décrite au début) :

uniquement pour les contextes de fonctions

  • pour chaque paramètre formel d'une fonction,
    • une propriété de l'objet des variables avec le nom et la valeur du paramètre formel est créée,
    • et pour les arguments non passés une propriété de l'objet des variables avec le nom et la valeur undefined est créée ;

pour le contexte global et les contextes de fonctions

  • pour chaque déclaration de fonction,
    • une propriété de l'objet des variables avec le nom et la valeur de l'objet fonction est créée (si l'objet des variables contient déjà une propriété avec ce nom, sa valeur est remplacée),
  • pour chaque déclaration de variable,
    • une propriété de l'objet des variables avec le nom et la valeur undefined est créée. Si le nom de la variable est le même que celui d'un paramètre formel ou qu'une déclaration de fonction cela n'affecte pas la propriété existante.

Voyons cela avec l'exemple suivant :

Code JavaScript

function precogs(reportA, reportB) {
    var minorityReport = 10;
    function agatha() {}
    var dashiell = function dash() {};
    (function arthur() {});
}

precogs(10); // appel

En entrant dans le contexte de la fonction precogs avec le paramètre 10, l'objet d'activation est le suivant :

Pseudo-code

AO(<precogs>) = {
    reportA: 10,
    reportB: undefined,
    minorityReport: undefined,
    agatha: <référence à la FD `agatha`>
    dashiell: undefined
}

Notons que cet objet d'activation ne contient pas la fonction arthur. Cela est dû au fait que arthur n'est pas une déclaration de fonction mais une expression de fonction (dont la forme abrégée sera FE pour « function expression »). Les expressions de fonction n'affecte pas l'objet des variables.

Cependant la fonction dash est également une expression de fonction, mais comme nous le verrons plus bas, parce qu'elle est assignée à la variable dashiell, elle devient accessible via le paramètre dashiell de l'objet des variables. La différence entre une déclaration de fonction et une expression de fonction sera vu plus en détail dans le chapitre approprié.

Et maintenant attaquons nous à la seconde phase du traitement du code d'un contexte d'exécution — la phase d'exécution du code.

Exécution du code

À ce stade un objet d'activation (ou l'objet global) est déjà rempli de ses propriétés (cependant, elles n'ont pas toutes leurs vraies valeurs car la plupart on encore la valeur undefined).

Considérez que tous les exemples fonctionnent pareil pour un objet d'activation ou l'objet global. Durant cette phase d'exécution, les valeurs sont alors modifiées comme suit :

Pseudo-code

AO['minorityReport'] = 10
AO['dashiell'] = <réference à la `FE` `dash`>

Rappelons encore que l'expression de fonction dash est encore en mémoire _seulement parce qu'elle a été sauvée dans la déclaration de variable dashiell. Mais l'expression de fonction arthur n'est pas dans l'objet d'activation. Si nous essayons d'appeler la fonction arthur avant ou même après sa définition, nous auront l'erreur "arthur" n'est pas défini(e). Les fonctions d'expression qui ne sont pas sauvées dans des variables ne peuvent être appelées qu'immédiatement ou à l'intérieur d'elles-même de manière récursive.

Voici un autre exemple classique :

Code JavaScript

alert(john); // `function john() {}`

var john = 10;
alert(john); // `10`

john = 20;

function john() {}

alert(john); // `20`

Pourquoi dans la première alerte john est une fonction qui est accessible avant sa déclaration ? Pourquoi ce n'est pas 10 ou 20 ? Parce que, selon les règles, l'objet des variables est rempli avec les déclarations de fonction en entrant dans le contexte. Pendant cette phase d'entrée dans le contexte il y a aussi une déclaration de variable john. Mais comme mentionné plus haut, cette étape est réalisée après les déclarations de fonction et les paramètres formels et cette phase n'affecte pas les propriétés existantes de même nom que ce soit pour les déclarations de fonction ou les paramètres formels. Donc en entrant dans le contexte l'objet des variables est rempli ainsi :

Pseudo-code

VO = {}

VO['john'] = <réference à la `FD` `john`>

// `var john = 10;` trouvée
// si la fonction `john` n'avait pas déjà été définie
// et bien `john` aurait été `undefined`, mais dans notre cas
// la déclaration de variable n'affecte pas
// la valeur de la fonction avec le même nom

VO['john'] = <la valeur n'ai pas affectée, toujours la `FD`>

Et ensuite, en entrant dans la phase d'exécution, l'objet des variables est modifié ainsi :

Pseudo-code

VO['john'] = 10
VO['john'] = 20

c'est ce que nous pouvons voir dans la deuxième et troisième alerte.

Dans l'exemple ci-dessous nous voyons de nouveau que les variables sont mises dans l'objet des variables pendant la phase d'entrée dans le contexte (ainsi la structure else n'est jamais exécutée, mais, la variable b existe dans l'objet des variables) :

Code JavaScript

if (true) {
    var reportA = 1;
} else {
    var reportB = 2;
}

alert(reportA); // `1`
alert(reportB); // `undefined`, mais pas « erreur : `reportB` n'est pas défini(e) »

Fonctionnalité des navigateurs : les contextes globaux

Comme dit plus haut, une page web n'exécute pas un Programme JavaScript mais autant de programmes qu'elle rencontre de balise <script>, d'éléments HTMLScriptElement ou encore de code à exécuté en tant que JavaScript (attribut onclick, href="javascripst: ...", etc.). Il en résulte qu'il n'existe pas « un » contexte global mais des contextes globaux. Cependant, tous ces contextes utilisent le même objet global.

Les phases d'entrée dans le contexte global et d'exécution du code global sont donc ré-itérées pour chaque nouveau programme (<script>, etc). Mais a chaque fois, l'objet global (déjà disponible dès l'entrée dans le contexte) contient les déclarations de variables récupérées des contextes globaux précédents. Parceque les contextes globaux sont activés dans le même ordre que les programmes sont trouvés et qu'il n'existe pas de phase « d'entrée dans la page web », toute déclaration de variables venant du contexte global d'un programme qui sera exécuté plus tard n'est pas accessible dans le contexte global courant. À l'inverse, toutes déclarations de variables venant du contexte global d'un programme qui a déjà été exécuté est disponible et accessible. C'est la même chose pour les déclarations de fonctions. Voyons cela avec ces exemples :

Code HTML

<script>
    // Le script `echo1` sera analysé en premier

    console.log(arthur); // `function arthur() {}`
    console.log(agatha); // « error : `agatha` n'est pas défini(e) »
    console.log(dashiell); // « error : `dashiell` n'est pas défini(e) »

    function arthur() {}
</script>
<script>
    // Le script `echo2` sera analysé en second

    console.log(arthur); // `function arthur() {}`
    console.log(agatha); // `10`
    console.log(dashiell); // `function dashiell() {}`

    var agatha = 10;
    function dashiell() {}
</script>

Cela remplit notre objet global consécutivement des manières suivantes : pour le premier script

Pseudo-code

GO(<echo1>) = {
    arthur: <référence à la FD `arthur`>
}

et pour le second script

GO(<echo2>) = {
    arthur: <référence à la FD `arthur`>, // déjà présent dans l'objet global et trouvé d'un précédent programme
    agatha: undefined,
    dashiell: <référence à la FD `dashiell`>
}

Nous voyons donc pourquoi lors de la phase d'exécution, le premier script retourne une erreur pour agatha et dashiell qui n'existeront dans l'objet global qu'après la phase d'entrée dans le contexte du script2.

Notez également que par conséquent, de la même manière que l'ordre des instructions d'un code est important, l'ordre dans lequel les scripts sont appelés l'est aussi car il peut changer le fonctionnement d'un programme.

Code HTML

<script>
    // Ce script `echo2` sera analysé en premier

    console.log(arthur); // « error : `arthur` n'est pas défini(e) »
    console.log(agatha); // `10`
    console.log(dashiell); // `function dashiell() {}`

    var agatha = 10;
    function dashiell() {}
</script>
<script>
    // Ce script `echo1` sera analysé en second

    console.log(arthur); // `function arthur() {}`
    console.log(agatha); // `10`
    console.log(dashiell); // `function dashiell() {}`

    function arthur() {}
</script>

à cause de l'état de l'objet global évoluant ainsi :

Pseudo-code

GO(<echo2>) = {
    agatha: 10,
    dashiell: <référence à la FD `dashiell`>
}

et

GO(<echo1>) = {
    agatha: 10, // déjà présent dans l'objet global et trouvé d'un précédent programme
    dashiell: <référence à la FD `dashiell`>  // déjà présent dans l'objet global et trouvé d'un précédent programme
    arthur: <référence à la FD `arthur`>
}

À propos des variables

On voit souvent dans différents articles (et même livres) sur le JavaScript l'affirmation suivante : « il est possible de déclarer une variable globale en utilisant le mot-clé var (dans le contexte globale) et sans le mot-clé var (depuis n'importe quel contexte) ». Ce n'est pas exactement ça. Soyez assuré de cela :

Les variables se déclarent uniquement avec le mot-clé var.

Et des affectations comme ci-dessous :

Code JavaScript

reportA = 10;

créent simplement une nouvelle propriété dans l'objet global mais pas une variable. « Pas une variable » mais pas dans le sens qu'elle ne pourra pas être changée lors de la phase d'exécution (on pourra y accéder car VO(globalContext) === GO) mais « pas une variable » dans le sens qu'elle n'est pas dans l'objet des variables lors de la phase d'entrée.

Regardons cet exemple pour tester la différence :

Code JavaScript

alert(reportA); // `undefined`
alert(reportB); // « erreur, `reportB` n'est pas défini(e) »

reportB = 10;
var reportA = 20;

La différence vient de l'objet des variables et des phases où il est modifié (phase d'entrée dans le contexte et phase d'exécution de code) :

Pour l'entrée dans le contexte :

Pseudo-code

VO = {
    reportA: undefined
}

Nous voyons qu'il n'existe aucune variable reportB. reportB n'apparaîtra que lors de la phase d'exécution du code (et dans notre cas reportB n'apparaîtra pas car il va y avoir une erreur).

Changeons le code :

Code JavaScript

alert(reportA); // `undefined`, nous savons pourquoi

reportB = 10;
alert(reportB); // `10`, créée lors de l'exécution du code

var reportA = 20;
alert(reportA); // `20`, modifiée lors de l'exécution du code

Voici le point important en ce qui concerne les variables. Les variables, par opposition aux simples propriétés, ont un attribut {DontDelete}, ce qui signifie qu'il est impossible de les supprimer avec l'opérateur delete :

Code JavaScript

reportA = 10;
alert(window.reportA); // `10`

alert(delete reportA); // `true`

alert(window.reportA); // `undefined`

var reportB = 20;
alert(window.reportB); // `20`

alert(delete reportB); // `false`

alert(window.reportB); // toujours `20`

Notons qu'en ES5 {DontDelete} a été renommé en [[Configurable]] et peut être manuellement gérer via l'utilisation de la méthode Object.defineProperty.

Cependant il y a un contexte d'exécution ou cette règle n'a pas d'effet. C'est le contexte eval : l'attribut {DontDelete} n'est pas appliqué pour les variables :

Code JavaScript

eval('var minorityReport = 10;');
alert(window.minorityReport); // `10`

alert(delete minorityReport); // `true`

alert(window.minorityReport); // `undefined`

Si en essayant votre code dans une console JavaScript vous pouvez supprimer une variable, c'est que cette console utilise eval pour évaluer votre code.

Fonctionnalité des moteurs : la propriété __parent__

Comme nous l'avons déjà mentionné, conformément au standard, accéder directement à l'objet d'activation est impossible. Cependant, dans plusieurs moteurs comme celui de Mozilla Firefox (SpiderMonkey) ou Rhino les fonctions ont une propriété spéciale __parent__ qui est une référence à l'objet d'activation (ou à l'objet global) dans lequel ces fonctions ont été créées.

Exemple (SpiderMonkey et Rhino) :

Code JavaScript

var GO = this;
var minorityReport = 10;

function agatha() {}

alert(agatha.__parent__); // `GO`

var VO = agatha.__parent__;

alert(VO.minorityReport); // `10`
alert(VO === GO); // `true`

Dans l'exemple ci-dessus nous voyons que agatha est créée dans le contexte global et sa propriété __parent__ est ajoutée à l'objet des variables du contexte global, c.-à-d. à l'objet global.

Cependant obtenir l'objet d'activation dans SpiderMonkey de la même manière n'est pas possible : cela dépendra de la version, mais __parent__ pour des fonctions dans des fonctions retournera null ou l'objet global.

Dans Rhino, l'accès à l'objet d'activation est permis et disponible de cette manière :

Exemple (Rhino) :

Code JavaScript

var GO = this;
var reportA = 10;

(function dashiell() {

    var reportB = 20;

    // l'objet d'activation du contexte `dashiell`
    var AO = (function () {}).__parent__;

    print(AO.reportB); // 20

    // __parent__ de l'objet d'activation
    // courant est déjà l'objet global,
    // c.-à-d. qu'une chaîne spéciale d'objets des variables est formée,
    // on la nomme : chaîne des portées
    print(AO.__parent__ === GO); // `true`

    print(AO.__parent__.reportA); // `10`

})();

Conclusion

Dans cet article nous avons vu plus en détail l'étude des objets relatifs aux contextes d'exécution. Les prochains chapitres seront dévoués à la résolution des identifiants, aux chaînes des portées et, par conséquent, aux fermetures en JavaScript.

Références

Section correspondante de la spécification ECMA-262-3 :

Ce texte est une libre ré-écriture française de l'excellent billet Тонкости ECMA-262-3. Часть 2. Объект переменных. de Dmitry Soshnikov.

Lire dans une autre langue