ES3 en détails

Tutoriel pour apprendre à utiliser la valeur de this en JavaScript

Bruno Lesieur

Dans ce tutoriel, nous allons discuter d'une propriété supplémentaire liée aux contextes d'exécution : le mot-clé this.

Commentez

- Titre : ES3 en détails

- Auteur : Bruno Lesieur

- Parution : 27 février 2018

- Mise à jour : 27 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

Sommaire

- I. Introduction

- II. Définitions

- III. La valeur de this dans le code global

- IV. La valeur de this dans le code des fonctions

- IV-A. Le type Reference

- IV-A-1. Les identifiants

- IV-A-2. Les accesseurs de propriété

- IV-A-3. Détermination de this

- IV-B. Appel de fonctions et types sans Reference

- IV-C. Le type Reference et la valeur null de this

- IV-D. La valeur de this dans une fonction appelée en tant que constructeur

- IV-E. Manuellement affecter la valeur de this à l'appel d'une fonction.

- V. Conclusion

- VI. Remerciements

I - Introduction

Beaucoup de développeurs associent le mot-clé this à ce qu'il est dans la programmation orientée objet, à savoir, une référence à un objet nouvellement créé par un constructeur. En JavaScript ce concept existe aussi, cependant il ne se limite pas uniquement à la référence d'un objet instancié.

Comme la pratique le montre, ce sujet est assez difficile et trouver quelle est la valeur de this à travers les différents contextes d'exécution est bien souvent problématique.

Voyons plus en détail toutes les possibilités offertes par le mot-clé this en JavaScript.

II - Définitions

this est une propriété du contexte d'exécution. C'est un objet spécial du contexte dans lequel le code est exécuté.

Pseudo-code

1. activeExecutionContext = {

2. VO: {

3. <...>

4. },

5. this: <objet dédiée ou GO>

6. }

Il y a l'objet des variables (dont la forme abrégée sera VO pour « variable object ») dont nous avons discuté dans le chapitre précédent.

Quant à this, il est directement lié aux déclinaisons de codes exécutables des contextes. La valeur de this est déterminée pendant la phase d'entrée dans le contexte et est immuable pendant que le code du contexte est exécuté.

Examinons les différents cas en détail.

III - La valeur de this dans le code global

Dans le code global, la valeur de this est toujours l'objet global (dont la forme abrégée sera GO pour « global object »). Il est donc possible d'y faire référence indirectement comme suit :

1. // Définition explicite d'une propriété de `this`

2. this.jack = 10; // === (GO.jack = 10)

3. console.log(jack); // `10`

5. // Définition implicite via l'assignation d'une propriété de l'objet global

6. sally = 20;

7. console.log(this.sally); // `20`

9. // Définition implicite via la déclaration d'une variable

10. // car l'objet des variables du contexte global est l'objet global lui-même

11. var zero = 30;

12. console.log(this.zero); // `30`

IV - La valeur de this dans le code des fonctions

Les choses sont plus intéressantes quand this est utilisé à l'intérieur d'une fonction. Ce cas est le plus compliqué et est la cause de beaucoup de peines.

La première (et sûrement la plus importante) chose à savoir sur le mot-clé this dans cette déclinaison de code est qu'ici elle n'est pas statiquement liée à la fonction.

Comme mentionné plus haut, la valeur de this est déterminée pendant la phase d'entrée dans le contexte et dans le cas d'une fonction, cette valeur peut être complètement différente à chaque fois (à chaque appel).

Cependant, une fois la phase d'exécution du code en cours, la valeur de this est immuable. C'est-à-dire qu'il n'est pas possible de lui affecter une nouvelle valeur, car ce n'est pas une variable (par opposition au langage de programmation Python, par exemple, dont l'objet self est explicitement défini et peut donc être redéfini à souhait pendant la phase d'exécution) :

1. var nightmare = { gift: 10 };

3. var christmas = {

4. gift: 20,

5. party: function () {

7. console.log(this === christmas); // `true`

8. console.log(this.gift); // `20`

10. this = nightmare; // « erreur : cette valeur ne peut être changée »

12. // console.log(this.gift); // `20`

13. // S'il n'y avait pas eu d'erreur, cette valeur aurait pu être `10` à la place

15. }

17. };

19. // pendant la phase d'entrée dans le contexte la valeur de `this`

20. // est déterminée comme se référant à `christmas` ; pourquoi ?

21. // nous en discuterons plus en détail plus bas

23. christmas.party(); // `true`, `20`

25. nightmare.party = christmas.party;

27. // cependant juste après, la valeur de `this` se réfère

28. // à `nightmare`, même si nous appelons la même fonction

30. nightmare.party(); // `false`, `10`

Alors qu'est-ce qui fait varier la valeur de this dans le code d'une fonction ? Sa façon d'être activée.

Dans une fonction appelée de manière standard, this est fourni par l'appelant (« caller ») qui active le code du contexte, c'est-à-dire le contexte parent qui appelle la fonction. Et la valeur de this est déterminée par la forme de l'expression appelante (en d'autres mots, par la forme syntaxique avec laquelle la fonction est appelée).

C'est réellement un point important, aussi, nous allons encore insister sur ce point une nouvelle fois afin de déterminer sans problèmes la valeur de this dans chaque contexte. La forme de l'expression appelante, c'est-à-dire la manière dont la fonction est appelée, est la seule chose qui influence la valeur de this dans le contexte appelé.

Cela signifie donc que si vous avez pu lire ceci dans différents articles (ou même des livres) sur le JavaScript :

« La valeur de this dépend de la manière dont la fonction est définie. Si c'est une fonction global, this se réfère à l'objet global, et si cette fonction est la méthode d'un objet, la valeur de this est toujours cet objet ».

Hé bien ces articles se trompaient.

Nous allons voir que même une fonction globale peut être activée avec différentes formes d'expression appelante qui influencent tout autant la valeur de this :

1. function gift() {

2. console.log(this);

3. }

5. gift(); // `GO`

7. console.log(gift === gift.prototype.constructor); // `true`

9. // mais en exécutant notre fonction sous une forme d'expression appelante différente,

10. // pour la même fonction, la valeur de `this` est différente

12. gift.prototype.constructor(); // vaut le prototype de la fonction soit `gift.prototype`.

Autre exemple. Il est aussi possible d'appeler une fonction définie en tant que méthode d'un objet, mais que la valeur de this ne fasse pas référence à cet objet :

1. var gift = {

2. toy: function () {

3. console.log(this);

4. console.log(this === gift);

5. }

6. };

8. gift.toy(); // `gift`, `true`

10. var exampleFunc = gift.toy;

12. console.log(exampleFunc === gift.toy); // `true`

14. // cependant une fois encore sous une nouvelle forme d'expression appelante

15. // à partir de la même fonction, nous avons une valeur de `this` différente

17. exampleFunc(); // `GO`, `false`

Maintenant que la vérité est (r)établie, voyons comment concrètement la forme de l'expression appelante influence la valeur de this.

Afin d'y répondre, il faut d'abord comprendre par quel mécanisme interne est déterminée la valeur de this. Pour cela, il est nécessaire de voir plus en détail un type interne du JavaScript — le type Reference.

IV-A - Le type Reference

En utilisant du pseudocode, une valeur de type Reference peut être représentée comme un objet avec deux propriétés : la base (c'est-à-dire l'objet auquel appartient la propriété) et le nom de la propriété pour cette base :

Pseudo-code

1. valueOfReferenceType = {

2. base: <objet de la propriété>,

3. propertyName: <nom de la propriété>

4. }

Notons que depuis ES5, une référence contient également une propriété nommée strict qui est une valeur indiquant si la référence doit être résolue en mode strict ou non.

'use strict';

// Accès à `jack`.
jack;

Pseudo-code

// Référence pour `jack`.
jackReference = {
base: GO,
propertyName: 'jack',
strict: true,
}

La valeur d'un type Reference peut seulement être de deux sortes :

1. Récupérée à partir d'un identifiant,

2. Récupérée à partir d'un accesseur de propriété.

IV-A-1 - Les identifiants

Les identifiants sont pris en charge par le processus de résolution d'identifiant qui sera vu plus en détail dans le chapitre sur la chaîne des portées. Nous allons tout de même voir ici que cet algorithme de résolution retourne toujours une valeur de type Reference (qui est un élément important pour trouver la valeur de this).

Les identifiants sont des noms de variables, des noms de fonctions, des noms de paramètres et des noms de propriétés non qualifiées (des propriétés de l'objet global accédées sans préfixe). Par exemple, en ce qui concerne les valeurs des identifiants suivants :

1. var jack = 10; // variable

2. function sally() {}; // fonction

3. zero = 'ghost'; // propriété non qualifiée

un résultat intermédiaire des opérations de résolution correspondant au type Reference serait le suivant :

Pseudo-code

1. jackReference = {

2. base: GO,

3. propertyName: 'jack'

4. }

6. sallyReference = {

7. base: GO,

8. propertyName: 'sally'

9. }

11. zeroReference = {

12. base: GO,

13. propertyName: 'zero'

14. }

Et pour obtenir la valeur réelle d'un objet depuis sa valeur de type Reference, il y a une méthode interne GetValue qui peut-être décrite en pseudocode comme suit :

Pseudo-code

1. function GetValue(value) {

3. // Quand on ne demande pas la valeur d'un type Reference

4. if (Type(value) != Reference) {

5. return value;

6. }

8. // Obtenir ce sur quoi pointe la référence

9. base = GetBase(value);

11. // Vérifier qu'elle ne pointe pas dans le vide

12. if (base === null) {

13. throw new ReferenceError;

14. }

16. // Obtenir cette valeur

17. return base.[[Get]](GetPropertyName(value));

19. }

Il y a une méthode interne [[Get]] qui retourne la valeur réelle de la propriété d'un objet, incluant au préalable l'analyse de l'héritage de cette valeur vis-à-vis de la chaîne des prototypes :

Pseudo-code

1. GetValue(jackReference) // `10`

2. GetValue(sallyReference) // `function sally() {}`

3. GetValue(zeroReference) // `'ghost'`

IV-A-2 - Les accesseurs de propriété

Les accesseurs de propriété utilisent aussi ce mécanisme et il y en a deux déclinaisons : la notation point (« dot ») (quand la propriété a un nom correct et connu à l'avance), ou la notation crochet droit (« bracket ») :

1. var barrel = {

2. lock: function shock() {}

3. }

5. // accesseurs

6. barrel.lock();

7. barrel['lock']();

Dont un retour de calcul intermédiaire de type Reference est :

Pseudo-code

1. barrelLockReference = {

2. base: barrel,

3. propertyName: 'lock'

4. }

6. GetValue(barrelLockReference) // `function shock() {}`

IV-A-3 - Détermination de this

Donc, reposons la question une dernière fois : en quoi la valeur de type Reference est liée à la valeur de this du contexte de la fonction ? C'est parti pour la révélation principale de cet article.

La règle générale de la détermination de la valeur de this dans un contexte de fonction est la suivante :

La valeur de this dans un contexte de fonction est fournie par l'appelant et déterminée en fonction de la forme de l'expression appelante (la manière dont la fonction est syntaxiquement appelée).

Si sur la partie à gauche des parenthèses appelantes (...), il y a une valeur de type Reference alors la valeur de this associée est celle contenue dans la base de cette valeur de type Reference.

Dans tous les autres cas (c'est-à-dire avec n'importe quelle autre valeur qui n'est pas un type Reference), la valeur de this est toujours mise à null. Mais comme il n'y a pas de sens à ce que la valeur de this soit null, cette valeur est implicitement remplacée par l'objet global.

Notons qu'en mode strict en ES5, la valeur de this n'est pas associée à l'objet global mais est mise à undefined.

Montrons un exemple :

1. function barrel() {

2. return this;

3. }

5. barrel(); // `this` vaut `GO`

Nous voyons que dans la partie gauche des parenthèses appelantes, il y a une valeur de type Reference (car barrel est un identifiant) :

Pseudo-code

1. barrelReference = {

2. base: GO,

3. propertyName: 'barrel'

4. }

Donc, la valeur de this est définie comme étant celle de la base de la valeur du type Reference, c'est-à-dire l'objet global.

Voyons que cela est similaire avec un accesseur de propriété :

1. var barrel = {

2. lock: function shock() {

3. return this;

4. }

5. };

7. barrel.lock(); // `this` vaut `barrel`

Nous avons de nouveau une valeur de type Reference avec pour base l'objet barrel qui va donc être utilisé comme valeur de this lors de la phase d'activation :

Pseudo-code

1. barrelLockReference = {

2. base: barrel,

3. propertyName: 'lock'

4. }

Cependant, activer la même fonction avec une autre forme d'expression appelante, nous montre que l'on a une autre valeur de this :

1. var oogieBoogie = barrel.lock;

2. oogieBoogie(); // `this` vaut `GO`

car oogieBoogie étant un identifiant, il produit une autre valeur de type Reference utilisée comme base (l'objet global) et donc pour la valeur this :

Pseudo-code

1. oogieBoogieReference = {

2. base: GO,

3. propertyName: 'oogieBoogie'

4. }

Maintenant, nous pouvons exactement dire pourquoi la même fonction activée sous différentes formes d'expression appelante a différentes valeurs de this. La réponse est qu'il y a une valeur intermédiaire de type Reference différente :

Cas des identifiants :

1. function barrel() {

2. console.log(this);

3. }

5. barrel(); // `GO`

Car,

Pseudo-code

1. barrelReference = {

2. base: GO,

3. propertyName: 'barrel'

4. }

Cas des accesseurs de propriété :

1. console.log(barrel === barrel.prototype.constructor); // true

3. // autre forme d'expression appelante

5. barrel.prototype.constructor(); // `barrel.prototype`

Car,

Pseudo-code

1. barrelPrototypeConstructorReference = {

2. base: barrel.prototype,

3. propertyName: 'constructor'

4. }

Voici encore un autre exemple classique avec une détermination dynamique de la valeur de this en fonction de la forme de l'expression appelante :

1. function barrel() {

2. console.log(this.lock);

3. }

5. var nightmare = { lock: 10 };

6. var christmas = { lock: 20 };

8. nightmare.gift = barrel;

9. christmas.gift = barrel;

11. nightmare.gift(); // `10`

12. christmas.gift(); // `20`

IV-B - Appel de fonctions et types sans Reference

Nous avons noté plus haut que dans le cas où la partie gauche des parenthèses appelantes est une valeur qui n'est pas de type Reference (mais de n'importe quel autre type), la valeur de this est automatiquement mise à null et, par conséquent, remplacée par l'objet global.

Imaginons un exemple avec une telle expression :

1. (function () {

2. console.log(this); // `null` => `GO`

3. })();

Dans ce cas, nous avons une fonction de type Object (d'instance Function) mais pas un type Reference (il n'y a aucun identifiant ou accesseur de propriété à résoudre), et donc la valeur de this est finalement remplacée par celle de l'objet global.

Voici des exemples plus complexes :

1. var barrel = {

2. lock: function () {

3. console.log(this);

4. }

5. };

7. barrel.lock(); // Premier cas

8. (barrel.lock)(); // Deuxième cas

9. (barrel.lock = barrel.lock)(); // Troisième cas

10. (false || barrel.lock)(); // Quatrième cas

11. (barrel.lock, barrel.lock)(); // Cinquième cas

Nous avons un accesseur de propriété dont la valeur intermédiaire devrait normalement être une valeur de type Reference nous fournissant en base l'objet barrel (premier cas), mais qui pourtant, dans certains cas, nous donne pour this une base valant GO (Autres cas ?).

Le point ici, c'est que les trois derniers appels (qui ont bien une partie à gauche des parenthèses appelantes) après l'application de certaines opérations ne retournent pas une valeur de type Reference.

- Avec le premier cas, tout est clair. Il n'y a sans l'ombre d'un doute un type Reference qui est résolu, et par conséquent, la valeur de this est la base de cette référence, c'est-à-dire barrel.

- Dans le deuxième cas, il y a un opérateur de groupement qui n'applique pas, comme nous l'avons vu plus haut, la méthode pour aller chercher la valeur réelle d'un objet depuis une valeur de type Reference, c'est-à-dire qui n'applique pas GetValue. Sachant cela, après l'évaluation de l'opérateur de groupement qui dans ce cas retourne simplement barrel.lock, nous obtenons de nouveau une expression à gauche dont la valeur sera de type Reference et dont la base sera pour this l'objet barrel.

- Dans le troisième cas, l'opération dans l'opérateur de groupement se fait avec l'opérateur d'affectation, à la différence d'une opération réalisée uniquement avec un opérateur de groupement, l'opérateur d'affectation appelle la méthode GetValue. La conséquence est que cela nous ramène, une fois l'expression complètement évaluée, une fonction de type Object (mais pas une valeur de type Reference). Dans ce cas this vaut null et, par conséquent, vaut l'objet global.

- De la même manière pour les quatrième et cinquième exemples, l'opérateur virgule et l'opérateur d'expression logique OR font appel à la méthode interne GetValue. Ils perdent leurs valeurs de type Reference pour retourner une valeur de type Object (une fonction). Donc this finit par valoir l'objet global.

IV-C - Le type Reference et la valeur null de this

Nous avons vu qu'il y a des cas où l'appel de l'expression se situant sur la partie gauche des parenthèses retourne bien une valeur intermédiaire de type Reference, mais qu'il y a aussi des cas où la valeur de this est à null (et donc remplacée par l'objet global). C'est ce qui arrive quand la base de la valeur du type Reference est un objet d'activation (dont la forme abrégée sera AO pour « activation object »).

Nous pouvons mettre en évidence cela dans un exemple avec une fonction interne à une fonction parente appelante. Comme nous l'avons vu dans le deuxième chapitre ; les variables locales, les fonctions internes et les paramètres formels sont stockés dans l'objet d'activation, ce qui donne :

1. function jack() {

2. function sally() {

3. console.log(this);

4. }

5. sally(); // Comme la base de `sally` est `AO(jack)`, `this` est remplacé par `GO`

6. }

L'objet d'activation retourne toujours une valeur de this équivalente à null (c'est-à-dire que le pseudocode AO(jack) est équivalent à null). Ici encore, nous en revenons à la description précédente indiquant que la valeur de this dans le cas où elle est null est l'objet global.

Une exception demeure cependant avec une fonction qui serait appelée dans la structure de contrôle with dans le cas où with contiendrait un nom de propriété de fonction. La structure de contrôle with ajoute ses objets en amont de la chaîne de portées, c'est-à-dire avant l'objet d'activation. Par conséquent, la base de la valeur de type Reference (par un identifiant ou un accesseur de propriété) n'est pas l'objet d'activation, mais l'objet de la structure de contrôle with. Au passage, cela n'est pas vrai uniquement pour des fonctions internes, mais également aux fonctions globales, car l'objet with masque les objets les plus hauts (objet global ou objet d'activation) dans la chaîne des portées :

1. var gift = 10;

3. with ({

5. jack: function () {

6. console.log(this.gift);

7. },

8. gift: 20

10. }) {

12. jack(); // 20

14. }

Car,

Pseudo-code

1. jackReference = {

2. base: __withObject,

3. propertyName: 'jack'

4. }

Une situation similaire intervient en appelant une fonction qui serait le premier argument de la structure de contrôle catch : dans ce cas, l'objet catch est aussi ajouté en amont de la chaîne des portées c'est-à-dire avant l'objet d'activation ou l'objet global. Cependant, ce comportement est reconnu comme un bug dans ECMA-262-3, car la nouvelle version du standard ECMA-262-5 rectifie cela. C'est-à-dire que la valeur de this pour un objet d'activation doit être équivalente à l'objet global et non plus à l'objet catch :

1. try {

2. throw function () {

3. console.log(this);

4. };

5. } catch (nightmare) {

6. nightmare(); // `__catchObject` en ES3, `GO` dans la version ES5

7. }

c'est-à-dire en ES3 :

Pseudo-code

1. eReference = {

2. base: __catchObject,

3. propertyName: 'nightmare'

4. }

mais en ES5 :

Pseudo-code

1. eReference = {

2. base: GO,

3. propertyName: 'nightmare'

4. }

On a également la même situation avec un appel récursif d'une expression de fonction nommée (plus de détails à propos des fonctions seront donnés dans un prochain tutoriel). Au premier appel de la fonction, la base de la valeur intermédiaire de type Reference est l'objet d'activation parent (soit l'objet global), mais dans les appels récursifs — la base devrait être un objet spécial stockant le nom optionnel de l'expression de fonction. Cependant, dans ce cas, la valeur de this associée est toujours l'objet global :

1. (function nightmare(jack) {

3. console.log(this);

5. !jack && nightmare(1); // « devrait » être un objet spécial, mais est toujours l'objet global

7. })(); // `GO`

IV-D - La valeur de this dans une fonction appelée en tant que constructeur

Il y a un cas de plus en rapport avec la valeur de this dans un contexte de fonction. On le nomme appel de fonction en tant que constructeur :

1. function Happy() {

2. console.log(this); // objet nouvellement créé plus bas et nommé `happy`

3. this.halloween = 10;

4. }

6. var happy = new Happy();

7. console.log(happy.halloween); // `10`

Dans ce cas, l'opérateur new appelle la méthode interne [[Construct]] de la fonction Halloween qui va appeler à son tour la méthode interne [[Call]] après chaque création d'un nouvel objet pour fournir une nouvelle valeur de this pour chacun d'eux.

IV-E - Manuellement affecter la valeur de this à l'appel d'une fonction.

Il y a deux méthodes définies dans Function.prototype (et donc accessibles par toutes les fonctions), permettant de spécifier la valeur de this manuellement lors de l'appel d'une fonction. Ces méthodes sont apply et call.

Chacune d'entre elles accepte en premier argument la valeur que va prendre this dans le contexte créé lors de l'appel. La différence entre ces deux méthodes est vraiment minime. Pour apply, le second argument est obligatoirement un objet sous forme de tableau (ou un objet s'en rapprochant comme la propriété arguments de l'objet d'activation) dont chaque élément dans l'ordre sera associé à chaque paramètre de la fonction de gauche à droite. Pour call, la méthode accepte n'importe quels arguments. Le seul paramètre obligatoire pour ses deux méthodes est le premier, c'est-à-dire la valeur que prendra this.

Exemple :

1. var sally = 10;

3. function halloween(jack) {

4. console.log(this.sally);

5. console.log(jack);

6. }

8. halloween(20); // `this === GO`, `this.sally === 10` et `jack === 20`

10. halloween.call({ sally: 20 }, 30); // `this === <premier argument>`, `this.sally === 20` et `jack === 30`

11. halloween.apply({ sally: 30 }, [40]) // `this === <premier argument>`, `this.sally === 30` et `jack === 40`

V - Conclusion

Dans ce tutoriel, nous avons discuté des fonctionnalités du mot-clé this en JavaScript (qui est vraiment une fonctionnalité par comparaison au C++ ou au Java). Dans le prochain chapitre de cette série, nous allons éclaircir un point plusieurs fois mentionné dans ce tutoriel : la chaîne des portées.

VI - Remerciements

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

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