ES3, Chap 3. — La valeur de this en JavaScript
Ce billet fait partie de la collection ES3 dans le détail et en constitue le Chapitre 3.
Dans cet article nous allons discuter d'une propriété supplémentaire liée aux contextes d'exécution : le mot-clé this.
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.
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
activeExecutionContext = {
VO: {
<...>
},
this: <objet dédiée ou GO>
}
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 types 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.
La valeur de this dans le code global
Dans le code du contexte 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 :
Code JavaScript
// Définition explicite d'une propriété de `this`
this.jack = 10; // === (GO.jack = 10)
console.log(jack); // `10`
// Définition implicite via l'assignation d'une propriété de l'objet global
sally = 20;
console.log(this.sally); // `20`
// Définition implicite via la déclaration d'une variable
// car l'objet des variables du contexte global est l'objet global lui-même
var zero = 30;
console.log(this.zero); // `30`
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 problèmes.
La première (et surement la plus importante) chose à savoir sur le mot-clé this dans ce type 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.-à-d. 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) :
Code JavaScript
var nightmare = { gift: 10 };
var christmas = {
gift: 20,
party: function () {
console.log(this === christmas); // `true`
console.log(this.gift); // `20`
this = nightmare; // « erreur : cette valeur ne peut être changée »
// console.log(this.gift); // `20`
// S'il n'y avait pas eu d'erreur, cette valeur aurait pu être `10` à la place
}
};
// pendant la phase d'entrée dans le contexte la valeur de `this`
// est déterminée comme se référent à `christmas` ; pourquoi ?
// nous en discuterons plus en détail plus bas
christmas.party(); // `true`, `20`
nightmare.party = christmas.party;
// cependant juste après, la valeur de `this` se réfère
// à `nightmare`, même si nous appelons la même fonction
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.-à-d. 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.-à-d. 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 dans différents articles (et même livres) sur le JavaScript ceci :
« La valeur de this dépend de la manière dont la fonction est définie.
- Si c'est une fonction globale, 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 influence tout autant la valeur de this :
Code JavaScript
function gift() {
console.log(this);
}
gift(); // `GO`
console.log(gift === gift.prototype.constructor); // `true`
// mais en exécutant notre fonction sous une forme d'expression appelante différente,
// pour la même fonction, la valeur de `this` est différente
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 :
Code JavaScript
var gift = {
toy: function () {
console.log(this);
console.log(this === gift);
}
};
gift.toy(); // `gift`, `true`
var present = gift.toy;
console.log(present === gift.toy); // `true`
// cependant une fois encore sous une nouvelle forme d'expression appelante
// à partir de la même fonction, nous avons une valeur de `this` différente
present(); // `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.
Le type Reference
En utilisant du pseudo-code, une valeur de type Reference peut être représentée comme un objet avec deux propriétés : la base (c.-à-d. l'objet auquel appartient la propriété) et le nom de la propriété pour cette base :
Pseudo-code
valueOfReferenceType = {
base: <objet de la propriété>,
propertyName: <nom de la propriété>
}
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.
Code JavaScript
'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 :
- Récupérée à partir d'un identifiant,
- Récupérée à partir d'un accesseur de propriété.
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éfix). Par exemple, en ce qui concerne les valeurs des identifiants suivants :
Code JavaScript
var jack = 10; // variable
function sally() {}; // fonction
zero = 'ghost'; // propriété non qualifiée
un résultat intermédiaires des opérations de résolution, correspondant au type Reference serait le suivant :
Pseudo-code
jackReference = {
base: GO,
propertyName: 'jack'
}
sallyReference = {
base: GO,
propertyName: 'sally'
}
zeroReference = {
base: GO,
propertyName: 'zero'
}
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 pseudo-code comme suit :
Pseudo-code
function GetValue(value) {
// Quand on ne demande pas la valeur d'un type `Reference`
if (Type(value) !== Reference) {
return value;
}
// Obtenir ce sur quoi pointe la référence
base = GetBase(value);
// Vérifier qu'elle ne pointe pas dans le vide
if (base === null) {
throw new ReferenceError;
}
// Obtenir cette valeur
return base.[[Get]](GetPropertyName(value));
}
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
GetValue(jackReference) // `10`
GetValue(sallyReference) // `function sally() {}`
GetValue(zeroReference) // `'ghost'`
Les accesseurs de propriété
Les accesseurs de propriété utilise aussi ce mécanisme : il y en a deux types : la notation point (« dot ») (quand la propriété a un nom correct et connu à l'avance), ou la notation crochet droit (« bracket ») :
Code JavaScript
var barrel = {
lock: function shock() {}
}
// accesseurs
barrel.lock(); // accès direct
barrel['lock'](); // accès indirect dites la résolution d'identifiant dynamique de propriété
Dont un retour de calcul intermédiaire de type Reference est :
Pseudo-code
barrelLockReference = {
base: barrel,
propertyName: 'lock'
}
GetValue(barrelLockReference) // `function shock() {}`
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 appelante (...), il y a une valeur de type Reference alors la valeur de this associée est celle contenu dans la base de cette valeur de type Reference.
Dans tous les autres cas (c.-à-d. 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.
Montrons un exemple :
Code JavaScript
function barrel() {
return this;
}
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 (nom de fonction)) :
Pseudo-code
barrelReference = {
base: GO,
propertyName: 'barrel'
}
Donc, la valeur de this est définie comme étant celle de la base de la valeur du type Reference, c.-à-d. l'objet global.
Voyons que cela est similaire avec un accesseur de propriété :
Code JavaScript
var barrel = {
lock: function shock() {
return this;
}
};
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
barrelLockReference = {
base: barrel,
propertyName: 'lock'
}
Cependant, activer la même fonction avec une autre forme d'expression appelante, nous montre que l'on a une autre valeur de this :
Code JavaScript
var oogieBoogie = barrel.lock;
oogieBoogie(); // `this` vaut `GO`
car oogieBoogie étant un identifiant, il produit une autre valeur de type Reference, utilisé comme base (l'objet global) et donc pour la valeur this :
Pseudo-code
oogieBoogieReference = {
base: GO,
propertyName: 'oogieBoogie'
}
Notons qu'en mode strict en ES5, la valeur de this n'est pas associée à l'objet global mais est mise à undefined.
Maintenant nous pouvons exactement dire pourquoi la même fonction activée sous différente forme 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
Code JavaScript
function barrel() {
console.log(this);
}
barrel(); // `GO`
car
Pseudo-code
barrelReference = {
base: GO,
propertyName: 'barrel'
}
Cas des accesseurs de propriété
Code JavaScript
console.log(barrel === barrel.prototype.constructor); // true
// autre forme d'expression appelante
barrel.prototype.constructor(); // `barrel.prototype`
car
Pseudo-code
barrelPrototypeConstructorReference = {
base: barrel.prototype,
propertyName: 'constructor'
}
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 :
Code JavaScript
function barrel() {
console.log(this.lock);
}
var nightmare = { lock: 10 };
var christmas = { lock: 20 };
nightmare.gift = barrel;
christmas.gift = barrel;
nightmare.gift(); // `10`
christmas.gift(); // `20`
Appel de fonctions et types sans Reference
Nous avons noté plus haut que dans le cas ou 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 tel expression :
Code JavaScript
(function () {
console.log(this); // `null` => `GO`
})();
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 :
Code JavaScript
var barrel = {
lock: function () {
console.log(this);
}
};
barrel.lock(); // Premier cas
(barrel.lock)(); // Deuxième cas
(barrel.lock = barrel.lock)(); // Troisième cas
(false || barrel.lock)(); // Quatrième cas
(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 dernier appels (qui on 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 donc par conséquent, la valeur de this est la base de cette référence. C.-à-d. 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éel d'un objet depuis une valeur de type Reference, c.-à-d. 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 ce 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 appel 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 OU 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.
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 ou 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 :
Code JavaScript
function jack() {
function sally() {
console.log(this); // `GO`
}
sally(); // Comme la base de `sally` est `AO(jack)`, `this` est remplacé par `GO`
}
L'objet d'activation retourne toujours une valeur de this équivalente à null (c.-à-d. que le pseudo-code 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 ou 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.-à-d. 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 pour des 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 :
Code JavaScript
var gift = 10;
with ({
jack: function () {
console.log(this.gift);
},
gift: 20
}) {
jack(); // 20
}
car
Pseudo-code
jackReference = {
base: __withObject,
propertyName: 'jack'
}
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.-à-d. 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.-à-d. que la valeur de this pour un objet d'activation doit être équivalent à l'objet global et non plus à l'objet catch :
Code JavaScript
try {
throw function () {
console.log(this);
};
} catch (nightmare) {
nightmare(); // `__catchObject` en ES3, `GO` dans la version ES5
}
c.-à-d. en ES3
Pseudo-code
nightmareReference = {
base: __catchObject,
propertyName: 'nightmare'
}
mais en ES5
Pseudo-code
nightmareReference = {
base: GO,
propertyName: 'nightmare'
}
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ées dans le chapitre 5). 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'expressions de fonction. Cependant, dans ce cas, la valeur de this associée est toujours l'objet global :
Code JavaScript
(function nightmare(jack) {
console.log(this);
!jack && nightmare(1); // « devrait » être un objet spécial, mais est toujours l'objet global
})(); // `GO`
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 :
Code JavaScript
function Happy() {
console.log(this); // objet nouvellement créé plus bas et nommé `happy`
this.halloween = 10;
}
var happy = new Happy();
console.log(happy.halloween); // `10`
Dans ce cas, l'opérateur new appel 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.
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 ces deux méthodes est le premier, c.-à-d. la valeur que prendra this.
Exemple :
Code JavaScript
var sally = 10;
function halloween(jack) {
console.log(this.sally);
console.log(jack);
}
halloween(20); // `this === GO`, `this.sally === 10` et `jack === 20`
halloween.call({ sally: 20 }, 30); // `this === <premier argument>`, `this.sally === 20` et `jack === 30`
halloween.apply({ sally: 30 }, [40]) // `this === <premier argument>`, `this.sally === 30` et `jack === 40`
Conclusion
Dans cet article 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 cet article : la chaîne des portées.
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. Часть 3. This. de Dmitry Soshnikov.