Le binding et this en JavaScript
Par Christophe Porteneuve • Publié le 7 mars 2022

Mise à jour le 22 mai 2022, 21:04

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…

  1. loguer « Salut moi c’est Bob »
  2. loguer « Salut moi c’est undefined »
  3. lever une ReferenceError
  4. lever une TypeError du genre « pas de propriété firstName sur undefined ?

(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 ?

  1. Loguer « Salut moi c’est Bob »
  2. Loguer « Salut moi c’est undefined »
  3. Lever une ReferenceError
  4. Lever une TypeError du genre « pas de propriété firstName sur undefined ?

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 exemple bob.greet.
    • Complément = on appelle la fonction à la volée avec l'opérateur (), par exemple bob.greet() (il peut bien sûr y avoir des arguments).
  • N'importe quoi d'autre (on référence la fonction sans l'appeler à la volée) : this vaudra undefined (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 une TypeError (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 et call pour appeler la fonction avec un this et des arguments précis ;
  • une méthode bind pour créer une fonction d'enrobage qui se « souviendra » du this à 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 !

Découvrez nos cours vidéo ! 🖥

Nos cours vidéo sont un complément idéal et bon marché à nos articles techniques et formations présentielles. Autour de Git, de JavaScript et d’autres sujets, retrouvez des contenus de très grande qualité à des prix abordables, spécialement conçus pour lever vos plus gros points de blocage.