Cet article est une traduction de JavaScript. The Core écrit par Dmitry Soshnikov. Javascript The Core est un sommaire détaillé de la série d’article “ECMA-262-3 in detail”.

  1. L'objet
  2. Le chaînage des prototypes
  3. Le constructeur
  4. La pile des contextes d'exécution
  5. Le contexte d'exécution
  6. L'objet des variables
  7. L'objet d'activation
  8. La chaîne des portées (Scope chain)
  9. Les closures (fermeture)
  10. La valeur this
  11. Conclusion

Commençons par étudier le concept d'objet, un fondamental d'ECMAScript.

L'objet

ECMAScript étant un langage orienté objet de haut niveau, il fonctionne avec des objets. Il possède aussi des primitives mais elles sont, si besoin est, converties en objet.

Un objet est une collection de propriétés qui possède un unique objet prototype. Le prototype peut avoir pour valeur soit un objet soit null.

Prenons pour exemple un objet simple. Le prototype d’un objet est référencé par la propriété interne [[Prototype]]. Dans le schéma ci-dessous, nous préférerons employer la notation en underscore __‹internal-property›__ au lieu des doubles crochets. Ceci tout particulièrement pour l'objet prototype __proto__ (qui est une propriété disponible mais non standard, disponible dans certains moteurs comme SpiderMonkey).

var foo = {
  x: 10,
  y: 20
};

Cet objet possède deux propriétés explicitement déclarées et une propriété __proto__ implicite qui est une référence au prototype de foo:

Figure 1
Figure 1. Un objet basique avec un prototype.

Mais pourquoi ces prototypes sont-ils requis ? Analysons le concept de la chaîne des prototypes (prototype chain).

Le chaînage des prototypes

Les prototypes sont des objets comme les autres et peuvent posséder leurs propres prototypes. Un prototype peut posséder une référence vers un autre prototype et ainsi de suite. Lorsque plusieurs prototype sont chaînés par des références non-nulles on dit qu'il s'agit d'une chaîne de prototype (prototype chain).

Une chaîne de prototype est une chaîne finie d'objet utilisé pour implémenter l'héritage et le partage de propriétés.

Examinons le cas où nous avons deux objets qui diffèrent sensiblement. Un langage bien conçu devrait nous permettre de réutiliser les caractéristiques similaires entre les deux objets sans nous répéter. Dans le cas d'un langage basé sur les classes, ce style de réutilisation du code est appelé l'héritage basé sur les classes (class-based inheritance) (une classe A possède les fonctionnalités similaires aux deux classes B et C. B et C héritent de A et implémentent chacune la fonctionnalité qui les différencie).

ECMAScript ne possède pas de concept de classe. Cependant, le style de réutilisation du code diffère très peu (il est même parfois plus flexible que dans un langage basé sur les classes) et est possible grâce à la chaîne des prototypes. Ce type d'héritage est appelé l'héritage par délégation (delegation based inheritance). Pour être plus précis, dans le cas d'ECMAScript, on parle d'héritage prototypal (prototype based inheritance).

Comme pour l'exemple avec les classes A, B et C, en ECMAScript nous créons les objets: a, b et c. L'objet a contient la partie commune aux objets b et c. Et b et c possèdent uniquement leurs propres propriétés et méthodes.

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z
  }
};

var b = {
  y: 20,
  __proto__: a
};

var c = {
  y: 30,
  __proto__: a
};

// appel de la méthode héritée
b.calculate(30); // 60
c.calculate(40); // 80

Facile n'est-ce pas ? Nous observons donc que b et c ont accès à la méthode calculate définie par l'objet a qui est possible grâce au chaînage prototypal.

La règle est simple: si une propriété ou une méthode n'est pas trouvée dans l'objet lui-même, la propriété/méthode sera recherchée dans la chaîne de prototype. Si la propriété n'est pas trouvée dans le prototype, alors une recherche aura lieu dans le prototype du prototype et ainsi de suite dans toute la chaîne de prototype (ce procédé est le même pour l'héritage basé sur les classes, lors de la résolution d'une méthode héritée, il y a un parcours de la chaîne des classes). La première propriété/méthode trouvée avec le même nom sera utilisée. Une propriété trouvée est appelée propriété héritée. Si la propriété n'est pas trouvée après le parcours intégral de la chaîne de prototype, la valeur undefined sera retournée.

Notons que la valeur de this est égale au contexte de l'objet original lorsque l'on accède à une méthode héritée (et non au contexte du prototype de l'objet dans lequel la méthode à été trouvée). Dans l'exemple ci-dessous this.y est récupéré depuis b et c mais pas a. Alors que this.x est récupéré depuis a grâce au mécanisme de chaînage prototypal.

Si un prototype n'est pas spécifié explicitement sur un objet alors la valeur par défaut de __proto__ sera assignée: Object.prototype. L'objet Object.prototype est le dernier maillon de la chaîne et possède lui aussi une propriété __proto__ qui a pour valeur null.

La figure suivante montre la hiérarchie de l'héritage entre nos objets a, b et c:

Figure 2. Une chaîne de prototype

Nous avons souvent besoin d'avoir des objets avec la même structure (le même groupe de propriétés) et des états différents. Pour répondre à cette problématique, la fonction constructeur permet de générer des objets suivant un pattern spécifié.

Le constructeur

A part créer des objets suivant un pattern spécifié, la fonction constructeur fait autre chose très utile. Il assigne automatiquement un (objet) prototype à chaque objet nouvellement créé. Ce prototype sera conservé dans la propriété ConstructorFunction.prototype.

Nous pourrions par exemple réécrire l'exemple précédent en utilisant une fonction constructeur pour générer les objets b et c et analyser le rôle que jouera l'objet a et son prototype Foo.prototype :

// fonction constructeur
function Foo(y) {
  // qui peut créer des objets
  // suivant un pattern spécifié.
  // C'est à dire qu'ils posséderont
  // leurs propres propriétés "y"
  // après leurs créations
  this.y = y;
}

// "Foo.prototype" est une référence vers
// le prototype qui sera assigné aux objets
// nouvellement créé. Il sera utilisé
// pour définir les méthodes et propriétés
// que nous souhaiterons partager ou hériter.
// Ainsi nous pouvons définir:

// la propriété héritée "x"
Foo.prototype.x = 10;

// et une méthode héritée "calculate"
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};

// maintenant créons nos objets
// "b" et "c" en utilisant le "pattern" Foo
var b = new Foo(20);
var c = new Foo(30);

// appel des méthodes héritées
b.calculate(30); // 60
c.calculate(40); // 80

console.log(

  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true

  // De plus, "Foo.prototype" créé automatiquement
  // une propriété "constructor" qui est en réalité
  // une référence vers la fonction constructeur;
  // Il est possible d'accéder au constructeur des
  // instances "b" et "c" via cette propriété héritée

  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo // true

  b.calculate === b.__proto__.calculate, // true
  b.__proto__.calculate === Foo.prototype.calculate // true

);

Le code ci-dessus peut être representé sous la forme d'un diagramme relationnel:

Figure 3. La relation entre constructeur et objet.

Ce diagramme montre que tous les objets possèdent un prototype. Le constructeur Foo possède sa propre propriété __proto__ qui est une référence vers Function.prototype qui elle même référence Object.prototype via sa propriété __proto__. Donc Foo.prototype est simplement une propriété explicite de Foo qui sera référencée comme prototype des objets (instance de Foo) b et c.

Nous venons donc de classifier notre code grâce à Foo. Nous pouvons appeler cette combinaison constructeur + prototype une “classe”. En Python, les classes dynamiques fonctionnent exactement de la même manière pour la résolution des propriétés et des méthodes. De ce point de vue les classes en Python sont juste un sucre syntaxique de l'héritage basé sur la délégation (delegation based inheritance) utilisé en ECMAScript. L'explication complète et détaillée de ce méchanisme est décrite dans le chapitre 7 des ES3 Series: Chapter 7.1. OOP. The general theory et Chapter 7.2. OOP. ECMAScript implementation.

Maintenant que nous avons vu l'aspect basique des objets, observons comment le système d'exécution (runtime program execution) est implémenté en ECMAScript. Avec ce que l'on appelle la pile des contextes d'exécution (execution context stack) où chaque élément qui y réside peut être représenté comme un objet. Eh oui, presque tout en ECMAScript est basé sur le concept d'objet ☺.

La pile des contextes d'exécution

Il y a trois types de code en ECMAScript: le code global, le code des fonctions et le code eval. Chaque code est évalué depuis son contexte d'exécution. Il y a un unique contexte global et il peut y avoir plusieurs instances des contextes d'exécution des fonctions et des evals. Chaque appel à une fonction évaluera son code avec le contexte d'exécution qui lui est propre. Chaque appel à la fonction eval évaluera son code avec le contexte d'exécution de eval.

Notons ici qu'une fonction peut générer une liste infinie de contexte (même si la fonction est appelée récursivement) car chaque appel produit un nouveau contexte avec un nouvel état:


function foo(bar) {}

// chaque appel de foo
// génère trois contextes différents
// avec des états différents
// (ex: la valeur de l'argument bar)

foo(10);
foo(20);
foo(30);

Un contexte d'exécution peut activer un autre contexte. Par exemple une fonction appelle une autre fonction (ou le contexte global appelle une fonction globale) et ainsi de suite. Tout ceci est regroupé dans une pile (stack) appelée pile des contextes d'exécution (execution context stack).

Un contexte qui active un autre contexte est appelé un caller (appelant). Un contexte activé est appelé un callee (appelé). Un callee peut-être en même temps le caller d'un autre callee (par exemple une fonction appelée depuis le contexte global qui appelle une autre fonction).

Quand un caller active (appelle) un callee, le caller suspend son exécution et passe le contrôle au callee. Le callee est ajouté à la pile et devient le contexte d'exécution actif. Dès la fin de l'exécution du callee, le contrôle est retourné au caller et l'évaluation continue à nouveau avec son propre contexte (qui peut activer une fois de plus d'autres contextes) jusqu'à la fin de ses instructions et ainsi de suite. Un callee peut seulement retourner une valeur (via un return) ou quitter avec une exception. Une exception lancée (throw) mais non attrapée (catch) peut quitter un ou plusieurs contextes (qui seront enlevés successivement de la pile).

Tous les programmes d'exécution ECMAScript (program runtime) ont une pile des contextes d'exécution (execution context (EC) stack) où le contexte actif est situé en haut de la pile:

Figure 4. La pile des contextes d'exécution

Quand un programme démarre, il entre dans le contexte d'exécution global qui est à la fois la base et le premier élément de la pile. Ensuite le code global procède à plusieurs initialisations comme les créations des objets et des fonctions. Durant l'évaluation du contexte d'exécution global, son code pourra activer d'autres fonctions (déjà créées) qui ajouteront leurs contextes d'exécution dans la pile et ainsi de suite. Une fois l'initialisation faite, le système d'exécution (runtime system) attend que des événements se produisent (un click de souris par exemple), ce qui activera d'autres fonctions qui créeront d'autres contextes d'exécution.

Le schéma ci-dessus montre la modification de la pile des contextes d'exécution en fonction de l'entrée et de la sortie d'un contexte d'exécution EC1. Le contexte global est ici appelé Global EC (EC pour execution context).

Figure 5. Les changements dans la pile des contextes d'exécution

Voici donc comment les systèmes d'exécution (runtime system) d'ECMAScript gèrent l'exécution du code.

Pour plus d'information sur les contextes d'exécution en ECMAScript consultez Chapter 1. Execution context.

Comme nous l'avons dit précédemment, un contexte d'exécution dans la pile peut être présenté comme un objet. Voyons voir sa structure et quel type d'état (quelles propriétés) un contexte a-t-il besoin pour exécuter son code.

Le contexte d'exécution

Un contexte d'exécution peut-être presenté comme un objet simple. Chaque contexte d'exécution possède une liste de propriétés qui sont nécessaires pour suivre la progression de l'exécution du code associé. Le schéma suivant décrit la structure d'un contexte:

Figure 6. La structure d'un contexte d'exécution

Il possède donc trois propriétés (l'objet de ses variables, une valeur this et la chaîne des portées (scope chain)). Un contexte d'exécution peut avoir des états supplémentaires suivant l'implémentation.

Regardons ces propriétés plus en détail.

L'objet des variables

L'objet des variables contient les données qui sont accessibles (dans la portée (scope)) depuis le contexte d'exécution. C'est un objet spécial qui est associé au contexte et qui stocke les variables et les fonctions définies dans le contexte.

Notons que les expressions de fonction (contrairement aux déclarations de fonction) ne sont pas inclues dans l'objet des variables.

L'objet des variables est un concept abstrait. Selon le type de contexte il est physiquement representé sous des objets différents. Par exemple, dans le contexte global, l'objet des variables correspond à l'objet global (c'est pourquoi nous avons la possibilité d'accéder à des variables globales simplement parce qu'elles sont des propriétés de l'objet global).

Prenons l'exemple suivant dans le contexte d'exécution global:


var foo = 10;

function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE

console.log(
  this.foo == foo, // true
  window.bar == bar // true
);

console.log(baz); // ReferenceError, "baz" n'est pas définie

Selon l'exemple ci-dessus l'objet des variables du contexte global aura les propriétés suivantes:

Figure 7. L'objet des variables global.

La fonction baz étant une expression de fonction elle n'est pas incluse dans l'objet des variables. C'est pourquoi nous avons eu une ReferenceError lorsque nous avons tenté d'y accéder en dehors de la fonction elle même.

Notons que contrairement à d'autres langages comme C ou C++, seules les fonctions créent de nouvelles portées (scope) en ECMAScript. Les variables et les fonctions encapsulées définies à l'intérieur du scope de la fonction ne sont pas directement visibles depuis l'extérieur et ne polluent pas l'objet des variables global.

eval crée aussi un nouveau contexte d'exécution (spécifique à eval). Cependant, eval utilise soit l'objet des variables global soit l'objet des variables de la fonction appelante (la fonction qui a appelé eval).

Et qu'en est-il des fonctions et de leurs objets variable ? Dans le contexte d'une fonction, un objet des variables est representé comme étant un objet d'activation.

L'objet d'activation

Quand une fonction est activée (appelée) par son appelant, un objet special appelé objet d'activation est créé. Il est rempli des paramètres formels et de l'objet special arguments. L'objet d'activation est ensuite utilisé comme étant l'objet des variables dans le contexte de la fonction.

L'objet d'activation (c'est à dire l'objet des variables d'une fonction) est identique à l'objet des variables, sauf qu'en plus de stocker les variables et les déclarations de fonction il stock aussi les paramètres formels et l'objet arguments.

Considérons l'exemple suivant:

function foo(x, y) {
  var z = 30;
  function bar() {} // Fonction declaration, FD
  (function baz() {}); // Fonction expression, FE
}

foo(10, 20);

Voici l'objet d'activation (AO) du contexte de la fonction foo:

Figure 8. Un objet d'activation.

Une fois de plus, la fonction baz n'est pas incluse dans l'objet des variables/activation car c'est une expression de fonction.

Passons maintenant au chapitre suivant. Comme vous le savez sans doute, il est possible en ECMAScript d'utiliser des fonctions imbriquées (ou inner functions) et d'accéder aux variables des fonctions parentes (ou du contexte global) depuis ces fonctions. Nous venons de décrire ce qu'était l'objet des variables (ou objet d'activation) dans le contexte d'exécution d'une fonction, passons maintenant à la chaîne des portées ou scope chain.

La chaîne des portées (Scope chain)

Une chaîne de portée (scope chain) est une liste d'objet qui est parcourue lorsqu'un identifiant (présent dans le code du contexte) est évalué.

La règle est une fois de plus simple et très similaire à celle du chaînage des prototypes. Si une variable n'est pas trouvée dans son propre scope (c'est à dire que si la variable n'est pas trouvée dans l'objet des variables/d'activation du contexte d'exécution actuel) une recherche débute dans la variable objet du parent et ainsi de suite.

Selon les contextes un identifiant peut être: le nom d'une variable, une fonction déclarée, un paramètre formel, etc. Si une fonction contient dans son code des identifiants qui ne correspondent pas à des variables locales (ou à une fonction locale ou à un paramètre formel) cette variable est appelée variable libre. Pour rechercher ces variables libres la chaîne des portées (scope chain) est utilisée.

Généralement la chaîne des portées est une liste de toutes les variables objets parentes avec en début de liste l'objet des variables/d'activation de la fonction. Cependant la chaîne des portées peut aussi contenir n'importe quel autre objet comme par exemple des objets ajoutés dynamiquement à la chaîne pendant l'exécution du contexte (comme les with-objects ou les objets spéciaux des clauses catch).

Lorsqu'il y a résolution (recherche) d'un identifiant, la chaîne des portées est parcourue en partant de l'objet d'activation puis ensuite (si l'identifiant n'est pas trouvé dans l'objet d'activation) jusqu'au sommet de la chaîne. Le processus de résolution est le même que pour la recherche dans une chaîne de prototype.

var x = 10;

(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x" et "y" sont des "variables libres"
    // et sont trouvées (dans la chaîne des
    // portées de bar) dans l'objet succédant
    // à l'objet d'activation de bar
    console.log(x + y + z);
  })();
})();

Nous pouvons supposer que le lien entre les objets de la chaîne des portées s'effectue via une propriété implicite __parent__ qui pointe vers l'objet suivant dans la chaîne. Cette implémentation peut être testée directement dans un environnement Rhino car il expose cette propriété. En utilisant le concept de __parent__ nous pouvons donc représenter l'exemple ci-dessus par le schéma suivant:

Figure 9. Une chaîne des portées (scope chain).

Remarquons que les variables d'objets parent sont enregistrées dans les propriétés [[Scope]] des fonctions.

A l'exécution du code, la chaîne des portées peut être augmentée à l'aide des objets créés par les déclarations with et des clauses catch. Du fait que ces objets sont de simples objets, ils peuvent posséder des prototypes (et donc une chaîne de prototype). Ce fait implique que la recherche dans la chaîne des portées s'effectue maintenant dans un espace à deux dimensions: pour chaque élément de la chaîne, la recherche s'effectue d'abord dans l'objet des variables de la portée puis dans le prototype de l'objet (s'il existe) et enfin dans l'objet parent et ainsi de suite.

Pour cet exemple de code:

Object.prototype.x = 10;

var w = 20;
var y = 30;

// Dans SpiderMonkey l'objet des variables
// du contexte global hérite de
// "Object.prototype"

console.log(x); // 10

(function foo() {

  // Variables locales de "foo"
  var w = 40;
  var x = 100;

  // "x" est trouvé dans "Object.prototype"
  // parce que {z:50} en hérite

  with ({z: 50}) {
    console.log(w, x, y , z); // 40, 10, 30, 50
  }

  // une fois que l'objet "with" est
  // supprimé de la chaîne des portées
  // "x" est encore present dans le
  // l'objet d'activation du contexte "foo";
  // de même que "w"
  console.log(x, w); // 100, 40

  // et voici comment accéder à la
  // variable globale cachée "w"
  // dans l'environnement d'un navigateur
  console.log(window.w); // 20

})();

nous obtenons la structure suivante:

Figure 10. Une chaîne des portées augmentée par with.

Notons que les implémentations d'ECMAScript ne voient pas forcément leur objet global hériter de Object.prototype. Le comportement décrit dans le schéma précédent (qui référence une variable non-définie x depuis le contexte global) peut être testé sous SpiderMonkey.

Il ne se passe rien de special lors de la récupération de données parentes depuis une fonction imbriquée (il s'agit juste d'un parcours de la chaîne des portées à la recherche de la donnée). Cependant, comme nous l'avons mentionné plus haut, dès qu'un contexte se termine, tout son état et lui même sont détruits. Dans le même temps une fonction imbriquée peut être retournée depuis la fonction parent. De plus, cette fonction pourrait très bien être activée plus tard depuis un autre contexte. Comment fonctionnera cette forme d'activation si le contexte de quelques variables libres est déjà détruit ? D'un point de vue théorique, le concept qui aide à résoudre ce problème est appelé une closure (lexical closure) et est en ECMAScript directement lié au concept de chaîne des portées (scope chain).

Les closures (fermeture)

En ECMAScript, les fonctions sont des objets de premier ordre (first-class objects). Cela signifie que les fonctions peuvent être passées en argument à d'autres fonctions (dans ce cas là on les appelle des "funargs" raccourci pour "functional arguments"). Les fonctions qui reçoivent des funargs sont appelées higher-order functions ou, plus proche du langage mathématique, des opérateurs. De même une fonction peut-être retournée par une autre fonction. Les fonctions qui retournent d'autres fonctions sont appelées function valued functions (ou des fonctions à valeur fonctionnelle).

Les "funargs" et "functional values" posent deux problèmes conceptuels. Ces deux sous-problèmes ont été généralisés en un seul, le "problème du funarg" (ou "Le problème de l'argument fonctionnel"). Et pour résoudre précisement ce problème, le concept de closure à été inventé. Décrivons maintenant plus en détail ces deux sous-problèmes (nous verrons par la suite qu'ils ont été résolus dans ECMAScript par la propriété [[Scope]] des fonctions).

Commençons par le sous-problème "upward funarg" (que l'on pourrait traduire par le "funarg ascendant"). Ce problème apparaît lorsqu'une fonction est retournée vers le haut (c'est à dire vers l'extérieur) depuis une autre fonction et qu'elle utilise des variables libres (type de variable déjà mentionné précédemment). Afin de pouvoir accéder aux variables du contexte parent (même une fois que le contexte parent se termine) au moment de la création de cette fonction imbriquée, ces variables sont enregistrées dans la propriété [[Scope]] de la chaîne des portées parent. Ensuite, quand la fonction est activée la chaîne des portées de son contexte aura pour valeur la combinaison de l'objet d'activation et de sa propriété [[Scope]].

chaîne des portées = Objet d'activation + [[Scope]]

Il faut retenir qu'une fonction sauvegarde, au moment de sa création, la chaîne des portées parente. C'est cette même chaîne des portées qui sera utilisée pour la résolution des variables lors des prochains appels de la fonction.

function foo() {
  var x = 10;
  return function bar() {
    console.log(x);
  };
}

// "foo" retourne une fonction
// et cette fonction retournée
// utilise la variable libre "x"

var returnedFunction = foo();

// variable globale "x"
var x = 20;

// exécution de la fonction retournée
returnedFunction(); // 10, et non 20

Ce style de portée (scope) est appelé porté statique (ou lexicale). Dans cet exemple nous pouvons voir que la variable x est enregistrée dans le [[Scope]] de la fonction retournée bar. Il existe aussi la portée dynamique qui aurait eu pour conséquence dans le code précédent de résoudre la variable x comme ayant pour valeur 20 et non 10. Néanmoins la portée dynamique n'est pas utilisée dans ECMAScript.

La seconde partie du "funarg problem" est le problème "downward funarg" (le problème du funarg descendant). Dans ce cas un contexte parent peut exister mais peut générer une ambiguïté lors de la résolution d'un identifiant. Le problème est le suivant: depuis quelle portée doit-on résoudre la valeur d'un identifiant ? La portée enregistrée statiquement lors de la création de la fonction ou la portée formée dynamiquement à l'exécution (la portée de l'appelant) ? Afin d'éviter toute ambiguïté et pour former une closure il a été décidé d'utiliser une portée statique:

// global "x"
var x = 10;

// fonction globale
function foo() {
  console.log(x);
}

(function (funArg) {

  // "x" local
  var x = 20;

  // il n'y a aucune ambiguïté
  // car nous utilisons la variable
  // globale "x" qui a été statiquement
  // enregistrée dans le [[Scope]] de
  // la fonction "foo" et non
  // le "x" de la portée de l'appelant
  // qui active "funArg"

  funArg(); // 10, et non 20

})(foo); // passage descendant de foo
// en tant que "funarg"

Nous pouvons conclure qu'une portée statique est obligatoirement requise pour chaque langage qui souhaite supporter les closures. Cependant, certains langages proposent une mélange entre portées dynamiques et statiques permettant ainsi au programmeur de choisir ce qu'il faut fermer (closure) et ce qu'il ne faut pas. Du fait que seule la portée statique est utilisée en ECMAScript (et qu'une solution à été trouvée pour les deux sous-problèmes du problème "funarg") nous pouvons en conclure qu'ECMAScript possède un support complet des closures et qu'elles sont implémentées par l'ajout d'une propriété [[Scope]] à chaque fonction. Nous pouvons donc maintenant donner une définition plus exacte de ce qu'est une closure:

Une closure est la combinaison d'un bloc de code (une fonction en ECMAScript) et des portées parentes statiquement/lexicalement enregistrées. Une fonction peut facilement résoudre ses variables libres à l’aide de ses portées enregistrées.

Du fait que chaque fonction enregistre son [[Scope]] lors de sa création nous pouvons en déduire que théoriquement, toutes les fonctions en ECMAScript sont des closures.

Autre point important, plusieurs fonctions peuvent avoir la même portée parent. Dans ce cas, les variables stockées dans la propriété [[Scope]] sont partagées entre toutes les fonctions. Les modifications réalisées sur des variables dans une closure seront reflétées lors de la lecture de ces variables depuis une autre closure.

function baz() {
  var x = 1;
  return {
    foo: function foo() { return ++x; },
    bar: function bar() { return --x; }
  };
}

var closures = baz();

console.log(
  closures.foo(), // 2
  closures.bar()  // 1
);

Ce code peut être illustré par le schéma suivant:

Figure 11. Un [[Scope]] partagé.

C'est précisément cette caractérique qui crée parfois une confusion lors de la création de plusieurs fonctions dans une boucle. En utilisant la variable compteur de la boucle directement dans les fonctions créés, certains développeurs obtiennent parfois des résultats non désirés où chaque fonction possède la même valeur de la variable compteur. Maintenant nous savons pourquoi. En réalité ces fonctions possèdent le même [[Scope]] où la variable compteur possède sa dernière valeur assignée.

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

data[0](); // 3 et non 0
data[1](); // 3 et non 1
data[2](); // 3 et non 2

Plusieurs techniques existent pour résoudre ce type de problème. L'une d'elle consiste à ajouter un nouvel objet dans la chaîne des portées (par exemple en y ajoutant une nouvelle fonction).

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = (function (x) {
    return function () {
      alert(x);
    };
  })(k); // passage de la valeur de "k"
}

// les résultats sont corrects
data[0](); // 0
data[1](); // 1
data[2](); // 2

Passons maintenant au chapitre suivant qui concerne la dernière propriété d'un contexte d'exécution: la valeur this.

La valeur this

this est un objet spécial qui est lié au contexte d'exécution. Il peut être vu comme étant l'objet contexte.

N'importe quel objet peut-être utilisé comme valeur this du contexte. Il y a parfois des erreurs dans les articles et livres traitant du contexte d'exécution d'ECMAScript et en particulier de this. this est souvent décrit, par erreur, comme étant une propriété de l'objet des variables.

this est une propriété du contexte d'exécution et non une propriété de la variable objet.

Cette caractéristique est très importante car contrairement aux variables, this ne participe jamais au processus de résolution des identifiants. Lorsque l'on accède à this depuis le code, sa valeur est récupérée directement depuis le contexte d'exécution et sans aucun parcours de la chaîne des portées. La valeur de this est déterminée seulement une fois: lors de l'entrée dans le contexte.

Contrairement à ECMAScript, Python possède l'argument self dans ses méthodes. self est une simple variable qui est résolue de la même manière qu'un argument lambda et qui peut être changé durant l'exécution par une autre valeur. En ECMAScript il n'est pas possible d'assigner une nouvelle valeur à this, parce que, une fois de plus, ce n'est pas une variable et qu'elle n'est pas placée dans l'objet des variables.

Dans le contexte global, this est l'objet global lui-même (cela veut dire que this est égal à l'objet des variables):

var x = 10;

console.log(
  x, // 10
  this.x, // 10
  window.x // 10
);

Dans le cas du contexte d'une fonction, la valeur de this peut être différente pour chaque appel de fonction. Ici la valeur de this est fournie par l'appellant (le caller) via une expression d'appel (call expression) (c'est à dire la façon dont une fonction est activée). Par exemple, la fonction foo ci-dessous (l'appelée) est appelée depuis le contexte global (l'appelant). Regardons dans l'exemple ci-dessous comment, pour le même code d'une fonction, la valeur de this diffère selon l'appelant:

// le code de la fonction "foo"
// ne change jamais, mais la valeur "this"
// diffère à chaque activation

function foo() {
  alert(this);
}

// l'appelant active "foo" (l'appelé)
// et lui fourni "this"

foo(); // objet global
foo.prototype.constructor(); // foo.prototype

var bar = {
  baz: foo
};

bar.baz(); // bar

(bar.baz)(); // bar aussi
(bar.baz = bar.baz)(); // mais ici il s'agit de l'objet global
(bar.baz, bar.baz)(); // toujours l'objet global
(false || bar.baz)(); // l'objet global ici aussi

var otherFoo = bar.baz;
otherFoo(); // encore l'objet global

Afin de comprendre précisément pourquoi (et surtout comment) la valeur de this peut changer suivant les appels, référez-vous au Chapter 3. This où tous les cas mentionnés précédemment sont étudiés plus en détail.

Conclusion

Nous venons de terminer ce bref aperçu d'ECMAScript 3 (bien que l'explication de tous ces sujets demanderait un livre complet). Nous n'avons pas abordé deux sujets majeurs: les fonctions (et la différence entre leurs différents types, function declaration et function expression) et la stratégie d'évaluation utilisée en ECMAScript. Ces deux sujets sont traités dans Chapter 5. Fonctions et Chapter 8. Evaluation strategy.

Relecteurs: Pierre Bertet et Pierre Romera.

Type de lecteur: Développeurs expérimentés, professionnels.