Pour le premier article de cette nouvelle année, nous allons nous intéresser au fonctionnement du mot-clé this
en JavaScript. Bien qu’il puisse paraître simple au premier abord, celui-ci se révèle bien plus complexe qu’il en a l’air notamment pour les développeurs venant d’autres langages.
Une histoire de contexte…
Dans la plupart des langages de programmation orientés objet tels que PHP , Java ou encore C++, this
fait référence à l’instance actuelle d’une classe. En JavaScript, il ne désigne pas nécessairement l’objet courant, mais dépend plutôt du contexte dans lequel il est utilisé, ce qui est souvent source d’erreur et d’incompréhension. Faisons un peu le tour des différents types de contextes.
Le contexte global
Lorsque this
est appelé en dehors de toute fonction, l’objet sur lequel celui-ci pointe est différent suivant l’environnement d’exécution.
Le navigateur web
Dans le cas d’une exécution dans le navigateur web, this
fait référence à l’objet global window
:
1 | console.log(this === window); // true |
Node.js
Bien que Node.js possède un objet global nommé global
, this
ne fait pas référence à celui-ci. Node.js utilise le principe de module, le code que vous écrivez sera donc exécuté à l’intérieur d’un module. De ce fait, this
fait référence à l’objet exports
(ou module.exports
qui est le même objet) :
1 2 | console.log(this === global); // false console.log(this === exports); // true |
Pour faire simple, lors de la création d’un module, Node.js encapsule votre code dans une fonction afin de ne pas polluer l’espace global (l’objet global
) et y injecte l’objet exports
comme valeur pour this
. Pour les plus téméraires d’entre vous, je vous invite à aller voir directement le code source de Node.js concernant les modules : module.js
Le contexte d’une fonction
JavaScript utilise en interne un mécanisme appelé « référence ». Ce mécanisme permet la résolution d’identifiant ou de propriété d’objet. Il utilise trois propriétés qui sont :
base
: Cette propriété peut prendre plusieurs valeurs :- L’objet qui fait appel à un accesseur de propriété (pour faire simple, ce qui se trouve à gauche du »
.
» ou »[]
» ); - L’objet global dans le cas contraire.
- L’objet qui fait appel à un accesseur de propriété (pour faire simple, ce qui se trouve à gauche du »
name
: Représente l’identifiant ou le nom de la propriété de l’objet;strict
: Indique simplement si le mode strict est activé ou non.
Ainsi dans le cas d’un appel d’une fonction, une référence est créée et permet à JavaScript de connaître le nom de la fonction à appeler, il s’agit de la propriété name
de la référence, mais également la valeur de this
à l’intérieur de cette fonction puisqu’il s’agit simplement de la propriété base
. Pour vraiment simplifier, vous pouvez imaginer que JavaScript remplace en interne, votre appel de fonction par :
1 | (base, name, strict)(); |
Vous êtes sûrement déjà tombé sur l’erreur ReferenceError
lors de l’appel à une fonction qui n’existe pas :
1 | notExistsFunction(); // ReferenceError: notExistsFunction is not defined |
C’est tout simplement que la référence créée ne permet pas la résolution de l’identifiant notExistsFunction
.
Par souci de simplicité et de clarté, nous représenterons, dans la suite de l’article, la référence sous la forme d’un objet littéral :
1 2 3 4 5 | const Reference = { base: 'object or environment', name: 'identifier or property name', strict: false // or true }; |
Voyons maintenant les différents cas d’appels de fonctions.
Les fonctions « ordinaires »
Prenons le cas d’une fonction « ordinaire » :
1 2 3 4 5 | function test() { console.log(this); } test(); |
Lorsque nous appelons la fonction test
, une « référence » est créée. Notre objet Reference
ressemble donc à ceci :
1 2 3 4 5 | const Reference = { base: 'environment', name: 'test', strict: false }; |
La propriété name
a donc pour valeur test
. Le mode strict n’étant pas activé, la propriété strict
a pour valeur false
. Comme nous ne faisons pas appel à un accesseur de propriété lors de l’appel à la fonction test
, la propriété base
a pour valeur l’objet global qui dépend de l’environnement d’exécution.
Le navigateur web
Dans le cas d’une exécution dans le navigateur web, voici à quoi ressemble notre objet Reference
:
1 2 3 4 5 | const Reference = { base: window, name: 'test', strict: false }; |
La propriété base
a donc pour valeur l’objet global window
, de ce fait this
fait référence à
:window
1 2 3 4 5 | function test() { console.log(this === window); // true } test(); |
Node.js
Dans le cas de Node.js, voici à quoi ressemble notre objet Reference
:
1 2 3 4 5 | const Reference = { base: global, name: 'test', strict: false }; |
la propriété base
a donc pour valeur l’objet global
, de ce fait global
this
fait également référence à
global :
1 2 3 4 5 | function test() { console.log(this === global); // true } test(); |
Mode strict
Lors de l’utilisation du mode strict, this
est égal à undefined
afin de prévenir tout risque de modification de l’objet global :
1 2 3 4 5 6 7 | 'use strict'; function test() { console.log(this); // undefined } test(); |
Les méthodes d’un objet
Lorsque nous appelons une méthode d’un objet, nous faisons appelle à un accesseur de propriété :
1 2 3 4 5 6 7 | const obj = { test: function () { console.log(this === obj); } } obj.test(); // Nous utilisons bien l'accesseur de propriété "." |
Ainsi lors de l’appel à la méthode test
, une « référence » est créée et notre objet Reference
ressemble à ceci :
1 2 3 4 5 | const Reference = { base: obj, name: 'test', strict: false }; |
Comme je l’ai dit précédemment, la propriété base
et donc this
, a pour valeur l’objet qui fait appel à un accesseur de propriété. Dans cet exemple, l’objet obj
utilise l’accesseur de propriété « .
« , this
dans la fonction test
, fait donc référence à cet objet. Par contre dans l’exemple suivant, this
ne fait pas référence à l’objet obj
, mais à l’objet global :
1 2 3 4 5 6 7 8 9 | const obj = { test: function () { console.log(this === obj); } } const unbindTest = obj.test; obj.test(); // true unbindTest(); // false |
On aurait pu s’attendre à ce que this
fasse référence à l’objet obj
, mais lors de l’affectation de la méthode test
à la variable unbindTest
, nous « brisons » le lien avec l’objet obj
car nous ne faisons pas appel à un accesseur de propriété lors de l’appel à la fonction unbindTest
. S’il y a une chose à retenir c’est que this
fait référence à ce qui se trouve à gauche d’un accesseur de propriété que ce soit la notation point « .
» ou crochets « []
« .
Le constructeur
La valeur de this
, dans le cas d’un constructeur est très simple. Il correspond à l’instance de l’objet qui vient d’être créé lors de l’appel à l’opérateur new
. Nous avons vu dans l’article sur l’héritage multiple comment fonctionnait cet opérateur. Pour rappel lors de l’appel à l’opérateur new
:
- Un nouvel objet est créé à partir de l’objet
prototype
du constructeur; - Le constructeur est appelé en lui fournissant comme valeur
this
l’objet nouvellement créé; - L’objet est retourné.
L’implémentation naïve de l’opérateur new
donnerait ceci :
1 2 3 4 5 | function newOperator(constr) { let o = Object.create(constr.prototype); constr.apply(o, Array.prototype.slice.call(arguments, 1)); return o; } |
Je reviendrais plus en détail sur la fonction apply
dans la suite de cet article.
Les fonctions fléchées
Nous avons vu que chaque fonction possède son propre this
. Ce qui peut parfois poser quelques soucis si l’on ne fait pas attention :
1 2 3 4 5 6 7 8 9 10 | const obj = { test: function(delay) { console.log(this === obj); // true setTimeout(function() { console.log(this === obj); // false }, delay); } }; obj.test(1000); |
Dans cet exemple, le premier this
que nous rencontrons fait référence à l’objet obj
. Jusqu’ici, il n’y a pas de soucis, c’est ce que l’on a vu précédemment. Par contre, dans la fonction de callback de setTimeout
, this
ne fait pas référence à l’objet obj
, mais à l’objet global. Pourquoi ? Et bien c’est exactement le même cas que l’affectation d’une méthode d’un objet à une variable vu précédemment. Le lien qui existe avec l’objet est « brisé ». En effet, lors de l’appel de cette fonction, aucun accesseur de propriété n’est utilisé. Pour résoudre ce problème, nous devons « sauvegarder » la valeur de this
comme ceci :
1 2 3 4 5 6 7 8 9 10 11 12 | const obj = { test: function(delay) { console.log(this === obj); // true const self = this; setTimeout(function() { console.log(self === obj); // true console.log(this === obj); // false }, delay); } }; obj.test(1000); |
Avec ECMAScript 6, il est possible de résoudre simplement ce problème en utilisant les fonctions fléchées. Une fonction fléchée contrairement aux autres fonctions ne possède pas sa propre valeur this
. La valeur de this
à l’intérieur d’une fonction fléchée fait référence au this
du parent. Voyons un exemple :
1 2 3 4 5 6 7 8 9 10 | var obj = { test: function(delay) { console.log(this === obj); // true setTimeout(() => { console.log(this === obj); // true }, delay); } }; obj.test(1000); |
La valeur de this
dans la fonction fléchée fait bien référence au this
du parent à savoir celui qui se trouve dans la fonction test
.
Injection de this
Avant de terminer cet article, nous allons voir qu’il est possible d’injecter la valeur de this
lors de l’appel d’une fonction. Nous allons pour cela voir trois méthodes qui sont call,
et apply
bind
.
call
La méthode
permet de faire appel à une fonction en lui fournissant comme paramètre la valeur de call
this
ainsi que la liste des arguments de la fonction :
1 | fun.call(thisArg[, arg1[, arg2[, ...]]]) |
Voici un exemple :
1 2 3 4 5 6 7 8 9 10 11 | const obj = { values: [1, 2, 3, 4, 5] }; function add(...values) { this.values = this.values.concat(values); } add.call(obj, 6, 7, 8, 9, 10); console.log(obj.values); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] |
Le premier paramètre de la méthode
est la valeur de call
this
, suivi des arguments de la fonction. Ainsi lors de l’appel à la fonction add
via la méthode call
, la valeur de this
fera référence à l’objet obj
. Si nous appelons directement la fonction add
une erreur se produit :
1 | add(6, 7, 8, 9, 10); // TypeError: Cannot read property 'concat' of undefined |
La valeur de this
dans la fonction add
fait référence cette fois-ci à l’objet global. Comme il ne possède pas de propriété values
, une erreur se produit.
apply
La méthode
fonctionne de la même façon que la méthode apply
à la différence que la liste des arguments n’est pas fournie individuellement, mais sous la forme d’un tableau :call
1 | fun.apply(thisArg, [argsArray]) |
Reprenons l’exemple précédent :
1 2 3 4 5 6 7 8 9 10 11 | const obj = { values: [1, 2, 3, 4, 5] }; function add(...values) { this.values = this.values.concat(values); } add.apply(obj, [6, 7, 8, 9, 10]); console.log(obj.values); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] |
bind
Tout comme
et call
, apply
permet d’injecter la valeur de bind
this
, mais contrairement aux deux autres méthodes qui appellent la fonction,
en créer une nouvelle. La méthode bind
possède la même signature que la méthode bind
call
:
1 | fun.bind(thisArg[, arg1[, arg2[, ...]]]) |
Reprenons l’exemple précédemment :
1 2 3 4 5 6 7 8 9 10 11 12 13 | const obj = { values: [1, 2, 3, 4, 5] }; function add(...values) { this.values = this.values.concat(values); } const objAdd = add.bind(obj, 6, 7, 8, 9, 10); objAdd(); console.log(obj.values); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] |
Peu importe la façon dont sera appelée la fonction créée, la valeur de this
à l’intérieur de celle-ci sera toujours celle fournie à la méthode
.bind
Pour conclure
J’espère que vous comprenez maintenant le fonctionnement du mot-clé this
et les différentes valeurs que celui-ci peut prendre suivant le contexte dans lequel il est utilisé. Pour résumer, il y a cinq choses que vous devez retenir afin de ne plus faire d’erreurs :
this
fait référence à ce qui se trouve à gauche d’un accesseur de propriété que ce soit la notation point ».
» ou crochets »[]
« ;- Si aucun accesseur de propriété est utilisé,
this
fait référence à l’objet global; - Dans un constructeur (via l’appel à l’opérateur
new
),this
fait référence à l’objet qui vient d’être créé; - Les fonctions fléchées ne possèdent pas de
this
; - Les méthodes
,call
etapply
permettent d’injecter la valeur debind
this
.
Si jamais vous avez des questions, n’hésitez surtout pas à les poser en commentaires.
Je suis lead developer dans une boîte spécialisée dans l’univers du streaming/gaming, et en parallèle, je m’éclate en tant que freelance. Passionné par l’écosystème JavaScript, je suis un inconditionnel de Node.js depuis 2011. J’adore échanger sur les nouvelles tendances et partager mon expérience avec les autres développeurs.
Si vous avez envie de papoter, n’hésitez pas à me retrouver sur Twitter, m’envoyer un petit email ou même laisser un commentaire.
Salut, super explications, je vais faire un tour sur les autres articles 😀
J’ai juste une question, à propos du paragraphe sur les fonctions fléchées, le tout premier exemple :
1 const obj = {
2 test: function(delay) {
3 console.log(this === obj); // true
4 setTimeout(function() {
5 console.log(this === obj); // false
6 }, delay);
7 }
8 };
9 obj.test(1000);
Vous dites :
J’ai testé pour me rendre compte du résultat, et je ne tombe pas sur l’objet global que j’aurais avec de simple fonction.
J’ai un objet Timeout {
_idleTimeout: 1,
_idlePrev: null,
…….
[Symbol(refed)]: true,
[Symbol(kHasPrimitive)]: false,
[Symbol(asyncId)]: 5,
[Symbol(triggerId)]: 1
}
Je me demandais pourquoi ? Est-ce la fonction setTimeout qui modifie le this de la fonction de callback ?
Et puis la structure est légèrement différente, cet objet a un nom « Timeout », ce qui n’est pas le cas de mes propres objets.
Merci 🙂
Je devine que tu as ce résultat sur Node. En fait, quand tu fais appelle à la fonction
setTimeout
sur Node, un objetTimeout
est créé en interne. De ce fait lethis
appelé dans la fonction callback desetTimeout
fait référence à l’instance de cet objetTimeout
. Les fonctions de timers de Node, bien qu’elles aient le même nom, fonctionnent en interne différemment de celles du navigateur. D’ailleurs la fonctionsetTimeout
coté navigateur est un « raccourci » dewindow.setTimeout
là on voit bien le contexte globalwindow
donc lethis
est bien attaché à ce contexte global c’est-à-dire l’objetwindow
Super, je comprends, merci beaucoup pour la réponse rapide 🙂👍