Le binding et this en JavaScript
Par Christophe Porteneuve • Publié le 7 mars 2022
• 8 min
Bienvenue dans notre neuvième article de la série JS idiomatique.
Aujourd’hui c’est du lourd, puisqu’on va régler la question du this
en JavaScript. Enfin presque, parce qu’on parlera des fonctions fléchées la prochaine fois, mais on va traiter disons… 95% du sujet aujourd’hui ! Si on revient à l’essentiel, le comportement fondamental de this
en JavaScript se résume à une phrase (courte) ! La prochaine fois que tu verras une personne soi-disant senior en JS se perdre dans des pseudo cas particuliers, tu pourras remettre ça au clair en un rien de temps, tu verras.
Tu préfères une vidéo ?
Si tu es du genre à préférer regarder que lire pour apprendre, on a pensé à toi :
Attends voir, “binding” ?
Même si c’est légèrement un abus de langage, avec le temps, le terme binding en JS a fini par désigner le mécanisme d’association (ou pas) d’une valeur à this
lors de l’exécution d’une fonction. Un temps, on a vu le terme « contexte » pour cette même notion, mais c’est un terme trop générique pour être utile, et aujourd’hui la plupart des API proposant ce type de réglage appellent le paramètre concerné thisArg
, ce qui est nettement plus explicite.
Un petit quiz pour commencer
Prends le code suivant :
const bob = {
firstName: 'Bob',
greet() {
console.log(`Salut moi c’est ${this.firstName}`)
},
}
À ton avis, le code bob.greet()
va…
- loguer « Salut moi c’est Bob »
- loguer « Salut moi c’est undefined »
- lever une
ReferenceError
- lever une
TypeError
du genre « pas de propriétéfirstName
surundefined
?
(Je précise que le comportement serait ici le même en mode laxiste et en mode strict.)
Réfléchis un peu, réponds, puis défile vers le bas pour lire la réponse. Ne triche pas !
Ça renverra bien « Salut moi c’est Bob ». Et très franchement, si ça faisait autre chose, on ne serait sans doute pas en train de parler de JavaScript, là. Le langage n’aurait jamais pris.
Bon, soit, mais prenons maintenant d’autres cas de figure. Je vais t’en donner plusieurs, qui vont tous se comporter pareil, car il s’agit en fait de quelques exemples d’un même cas général, déclinable autant qu’on veut :
// Et d'une
setTimeout(bob.greet, 0)
// Et de deux
;['blah'].forEach(bob.greet)
// Et de trois
requestAnimationFrame(bob.greet)
N’importe lequel de ces exemples ferait quoi, à terme, à ton avis ?
- Loguer « Salut moi c’est Bob »
- Loguer « Salut moi c’est undefined »
- Lever une
ReferenceError
- Lever une
TypeError
du genre « pas de propriétéfirstName
surundefined
?
Tu sais quoi faire…
Ça dépend du mode d’exécution de ton JS.
Allons bon, v’là autre chose.
En mode laxiste (qui est le mode par défaut des moteurs JS, ça vend la confiance), dans tous ces cas au sein de la fonction exécutée, this
vaudrait l’objet global (par exemple, dans le thread JS principal d’une page web classique, ce serait window
). Cet objet global existe, on a donc le droit d’en lire les propriétés. Il n’a en revanche pas de propriété firstName
(sauf si tu fais des trucs louches), de sorte que this.firstName
vaudra undefined
, et c’est donc la réponse 2 qui sera juste.
En mode strict (qui doit être explicite dans ton code, mais est aussi le mode utilisé par défaut pour le code transpilé par Babel ou TypeScript, notamment), this
serait ici undefined
, de sorte que this.firstName
lèverait une erreur : c’est la réponse 4 qui marche.
Le comportement strict est nettement plus utile, car il permet du fail-fast : s’assurer que le code va échouer immédiatement, de façon visible (une exception est levée), au plus près du code problématique (notre façon d’appeler bob.greet
). Sans ça, on peut très bien avoir du code erroné (qui manipule undefined
au lieu de la donnée souhaitée) mais dont on ne repère les effets délétères que longtemps après dans l’exécution du programme. Pas glop.
C’est défini à l’appel !
Déjà, je rappelle qu’on ne parle pas ici des fonctions fléchées, mais des autres : celles déclarées avec function
ou en méthodes concises dans un littéral objet ou corps de classe.
Dans tous les langages orientés objets, this
fait partie du contrat d’appel de la fonction, au même titre que ses arguments. C’est une sorte de paramètre caché, encore que parfois il soit explicitement déclaré — comme le self
de Python, premier paramètre de toutes les méthodes.
Si dans la majorité des langages, this
est défini lexicalement (en fonction du type de déclaration de la fonction dans le code), en JavaScript, this
est entièrement défini par l’appel. C’est la façon dont on appelle la « méthode » qui définit ce que vaudra this
au sein de celle-ci.
Je mets des guillemets, car en JavaScript il n’y a pas de méthodes au sens classique du terme, uniquement des fonctions. Une fonction n’est intrinsèquement associée à aucun objet en particulier. Notamment, le fait que sa déclaration initiale figure au sein d’un littéral objet ou d’un corps de classe n’a absolument aucune importance en termes de this
. C’est là une différence majeure avec la plupart des langages orientés objets, et la source de beaucoup de confusion pour les personnes arrivant à JS depuis un autre langage.
Là tu vas me dire : « tu déconnes, Christophe, regarde, on arrivait bien à faire bob.greet()
, y’avait bien une méthode greet
sur bob
, là ! »
Héééééé non.
En fait, c’est juste que la syntaxe d’appel unObjet.unePropFonction(…)
définit le this
dans la fonction à unObjet
. Ce à quoi on s’attend, en somme. C’est très similaire à ce qui se passe en Python : quand tu fais obj.say("hello")
, ça appelle en fait ta fonction say
avec obj
comme premier argument self
puis "hello"
comme deuxième argument. Sauf que pour nous, this
ne figure pas dans la signature. En fait, this
fait partie du FER de notre fonction.
Le FER, pour Function Environment Record, est une sorte de portée dédiée mise en place pour l’exécution de notre fonction, qui a donc priorité sur la portée englobante. Pour nos fonctions non-fléchées, le FER contient notamment des définitions pour this
, arguments
, super
et new.target
. Que la fonction soit fléchée ou non, on y trouve aussi les valeurs des paramètres figurant dans la signature.
Tous ces éléments du FER sont définis par l’appel de la fonction. En particulier, this
ne connaît que deux cas de figure :
- Sujet-Verbe-Complément : le sujet devient le
this
.- Sujet = on référence un objet, par exemple
bob
. - Verbe = on indexe une de ses propriétés qui s’avère être une fonction (une « méthode »), au moyen d’un des opérateurs
.
,[]
,?.
ou?.[]
, par exemplebob.greet
. - Complément = on appelle la fonction à la volée avec l’opérateur
()
, par exemplebob.greet()
(il peut bien sûr y avoir des arguments).
- Sujet = on référence un objet, par exemple
- N’importe quoi d’autre (on référence la fonction sans l’appeler à la volée) :
this
vaudraundefined
(en mode strict) ou l’objet global (en mode laxiste).
C’EST. TOUT.
Si.
Si, si.
Dans les exemples « foireux » de tout à l’heure, si tu regardes bien, on référence la fonction, mais on ne l’appelle pas : c’est un autre code qui va l’appeler, ici les Web API setTimeout
et requestAnimationFrame
, ou encore l’implémentation interne de Array#forEach
. L’appel ne figure pas dans le « terme » de notre expression, l’opérateur parenthèses ne suit pas immédiatement l’indexation dans le code. Et ça n’a rien à voir avec le fait de passer la fonction en callback, synchrone ou non ; de fait, tu aurais le même souci avec ce code-là :
// Pareil, kaboom
;(true ? bob.greet : alert)()
Voyons si t’as compris
OK, maintenant que tu as cette connaissance toute neuve, voyons si tu t’en sers bien. Prenons le code suivant :
// Je te remets ce bon vieux Bob, tel quel.
const bob = {
firstName: 'Bob',
greet() {
console.log(`Salut moi c’est ${this.firstName}`)
},
}
const alice = {
firstName: 'Alice',
greet: bob.greet,
}
alice.greet() // ?
setTimeout(alice.greet, 0) // ?
Alors, les deux appels en bas, là, ils font quoi ? Réfléchis puis défile…
- Le premier appel utilisera bien le prénom Alice
- Le deuxième soit utilisera
undefined
(mode laxiste) soit lèvera uneTypeError
(mode strict)
Souviens-toi : greet
n’appartient pas à bob
! Elle n’appartient à personne ! Le fait que la propriété greet
de Alice référence bob.greet
signifie juste qu’il s’agira de la même fonction en mémoire que la propriété homonyme de bob
, point barre.
Forçage explicite à l’appel
On vient de voir le comportement par défaut du langage pour la définition de this
. Mais JS permet aussi de forcer, programmatiquement, le this
passé à une fonction, au même titre que ses arguments. On peut maîtriser l’intégralité du contrat d’appel. C’est d’ailleurs absolument indispensable pour implémenter des API où un this
précis fait partie du contrat, comme la plupart des émetteurs d’événements par exemple (éléments du DOM, flux, etc.).
Les fonctions sont en effet des objets (respire), des instances de Function
, qui possèdent entre autres :
- des méthodes
apply
etcall
pour appeler la fonction avec unthis
et des arguments précis ; - une méthode
bind
pour créer une fonction d’enrobage qui se « souviendra » duthis
à utiliser par la suite.
La seule différence entre apply
et call
réside dans la façon de passer les arguments : pour call
, ce sont des arguments fournis individuellement, alors que pour apply
on passe un tableau générique (un objet qui a une tête de tableau : une propriété length
numérique non-négative et des propriétés numériques entre 0 et length - 1
; ça concerne tous les types de tableaux ainsi que le type Arguments
, notamment).
On peut donc très bien faire des trucs vaudou :
alice.greet.call(bob) // => Utilisera "Bob"
bob.greet.call(alice) // => Utilisera "Alice"
const fakeArray = { 0: 'VB', 1: 'mort', 2: 'À', length: 3 }
Array.prototype.reverse.call(fakeArray)
Array.prototype.join.call(fakeArray, ' ') // => "À mort VB"
Array.prototype.push.apply(fakeArray, ['et', 'COBOL']) // => 5
fakeArray // => { 0: 'À', 1: 'mort', 2: 'VB', 3: 'et', 4: 'COBOL', length: 5 }
Ça semble totalement farfelu, et pourtant sans ça, à peu près 99% des API Web et des bibliothèques et frameworks incontournables n’existeraient pas.
Lis la doc, bon sang !
Il arrive qu’on ait besoin de conserver le this
en vigueur au sein d’une fonction de rappel. Par exemple :
const bob = {
firstName: 'Bob',
greet(people) {
people.forEach(function (whom) {
console.log(`Salut ${whom} moi c’est ${this.firstName}`)
})
},
}
bob.greet(['Alice', 'Claire', 'David'])
// Salut [Alice|Claire|David] moi c'est undefined (ou TypeError)
// (t'avais qu'à faire un for-of, aussi !
Le souci est qu’un callback est une fonction passée par référence : aucun this
spécifique ne lui sera associé, ce sera de l’objet global ou undefined
, suivant le mode.
Je vois trop de gens encore contourner ça via une des astuces grossières habituelles :
// Approche 1 : backup de this (MEH)
const bob = {
firstName: 'Bob',
greet(people) {
const that = this
people.forEach(function (whom) {
console.log(`Salut ${whom} moi c’est ${that.firstName}`)
})
},
}
// Approche 2 : le bulldozer de bind (ça revient à peu près au même
// que l'approche 1, conceptuellement).
const bob = {
firstName: 'Bob',
greet(people) {
people.forEach(
function (whom) {
console.log(`Salut ${whom} moi c’est ${this.firstName}`)
}.bind(this)
)
},
}
Dans certains cas, c’est vrai, on n’aura pas le choix. Mais plus souvent qu’on ne le croie, l’API qu’on utilise nous permet de préciser le this
via un argument dédié. C’est le cas notamment de toutes les méthodes itératives de Array
(ex. forEach
, map
, filter
, some
…), dont le dernier argument (optionnel) est le thisArg. Dans le cas précédent, si on avait lu la doc, on aurait pu le faire en plus court et plus performant :
// Approche 3 : j'ai lu la doc 😎
const bob = {
firstName: 'Bob',
greet(people) {
people.forEach(function (whom) {
console.log(`Salut ${whom} moi c’est ${this.firstName}`)
}, this) // Hop, 2e argument !
},
}
Et les fonctions fléchées, alors ?
J’entends souvent des devs dire que les fonctions fléchées conservent le this
en vigueur parce qu’elles « auto-bindent ». On verra dans le prochain article que c’est en fait tout le contraire, et que les fonctions fléchées ne sont pas une panacée dans leur gestion du this
, qui est en fait extrêmement simple à expliquer… mais peut produire de sacrées surprises !
Envie de creuser davantage ?
Notre bon vieux cours vidéo sur this
en JavaScript explore tout ça plus en profondeur sur à peu près 1h30, avec moult variations, astuces, ProTips™ et schémas animés.
Ça t’a plu ? Ne manque pas la suite !
Il y a aussi encore tout plein de sujets merveilleux à venir dans cette série JS idiomatique.
Pour être sûr·e de ne rater aucun de nos tutos et articles, le mieux est encore de t’abonner à notre newsletter et à notre chaîne YouTube. Tu peux aussi nous suivre sur Twitter.
Et bien entendu, n’hésite pas à jeter un œil à nos formations ! Si explorer l’intégralité des recoins du langage t’intéresse, la ES Total notamment est faite pour toi !