Appeler une fonction JavaScript avec un this explicite
Par Christophe Porteneuve • Publié le 28 févr. 2020

Cet article est également disponible en anglais.

Aaaah, le this en Javacript. Ce n’est pas que le sujet soit affreusement compliqué, c’est surtout que personne ne fait l’effort d’apprendre les quelques notions fondamentales du truc ; du coup, tout le monde se trimballe son modèle mental erroné, hérité de ses précédents langages.

On se plaint surtout du fait qu’en JavaScript, on a tout le temps l’impression de « perdre » son this. Le corollaire de ça est intéressant : si nos fonctions ne sont pas intrinsèquement liées à un this figé, cela veut dire que nous pouvons appeler une fonction avec un this spécifique, explicite, à notre goût. Eh oui, en JavaScript, this fait partie du contrat d’appel de la fonction, au même titre que ses arguments. Ce qui ouvre une foule de possibilités sympathiques.

Ce n’est pas ce que vous croyez

À une exception près, JavaScript ne définit pas automatiquement this lors de l’appel d’une fonction. En fait, en mode strict et faute d’une définition explicite, à part l’exception en question, this sera toujours undefined. Voilà !

Peu importe d'où vient la fonction, comment elle a été déclarée, etc. En JavaScript, this est défini au moment de l’appel de la fonction, et non par sa déclaration.

Cette fameuse exception, quelle est-elle ? Elle survient lorsqu’une fonction « traditionnelle » (déclarée avec le mot-clé function ou en syntaxe concise de méthode) est appelée avec la forme que j’appelle « sujet, verbe, complément » :

  1. Sujet : un objet est utilisé au début de l’expression
  2. Verbe : on indexe une propriété de l’objet, propriété dont la valeur est notre fonction ; notez qu’il peut s’agir d’une indexation directe (opérateur .) ou indirecte (opérateur []), ça n’a aucune importance.
  3. Complément : on appelle la fonction obtenue immédiatement, à la volée dans l’expression (avec l’opérateur () contenant les éventuels arguments).

Prenons par exemple le code suivant :

const wife = {
  name: 'Élodie',
  greet() {
    return `Salut moi c’est ${this.name} !`
  },
}

Si je fais :

wife.greet() // => 'Salut moi c’est Élodie !'

Ça fonctionne bien. En décomposant l’expression, on trouve bien :

  1. Le sujet : wife
  2. Le verbe : greet
  3. Le « complément » : l’appel à la volée avec ()

Dans ce cas et dans ce cas seulement, JavaScript définit (entre autres) this dans le cadre de cet appel (techniquement, il remplit 4 entrées supplémentaires du Function Environment Record pour l’appel) en se basant sur le sujet. Dans le cas qui nous occupe, this vaudra wife, du coup dans la construction du texte, this.name vaudra wife.name, donc 'Élodie'.

« Perdre » son this

100% des autres cas reviennent en fait à référencer la fonction sans l’appeler, en tout cas au sein du terme d’expression concerné. Les possibilités sont infinies, par exemple :

// Aïe, ça marche pas™
setTimeout(wife.greet, 0)

// Non plus
navigator.plugins.forEach(wife.greet)

// Grmbl !
const f = wife.greet
f()

Le plus énervant, c’est lorsqu’on est dans une fonction de rappel depuis un contexte dont le this était le bon : la fonction de rappel est, par définition, passée sans être appelée à la volée ; c’est le mécanisme qui reçoit la fonction de rappel qui, justement, l’appellera le moment venu. Et là, kaboom !

const wife = {
  name: 'Élodie',
  greet(whom) {
    return `Salut ${whom} moi c’est ${this.name} !`
  },
  greetAll(people) {
    // Ici `this` sera bon…
    people.forEach(this.greet)
    // …mais `this.greet` n’étant pas appelée à la volée, une fois
    // dedans, `this` sera soit l’objet global (mode laxiste, par
    // défaut) soit `undefined` (mode strict, plus utile).
  },
}

Définir this : une partie du contrat d’appel

Finalement, en JavaScript, this fait partie du contexte d’invocation de la fonction, de son « contrat d’appel » en fait, au même titre que ses arguments (et notamment l’identifiant arguments, cette saleté) ou que super.

Mais alors, cela veut-il dire qu’on va pouvoir appeler une fonction en contrôlant explicitement son this ? Absolument !

Prenez la signature complète de Array#forEach par exemple :

forEach(callback[, thisArg])

Il est apparemment possible d’indiquer à forEach quel this utiliser le moment venu quand elle appellera notre fonction de rappel. Formidable !

const wife = {
  // …
  greetAll(people) {
    people.forEach(this.greet, this) // Presto ! Plus de problème…
  },
}

Mais comment donc forEach fait-elle ? Elle n’a sous la main qu’un identifiant dont la valeur est notre fonction de rappel, sans autre indication. Comment l’appelle-t-elle pour préciser un this en plus des arguments ?

Définir this quand on connaît les arguments

Elle pourrait utiliser une des méthodes disponibles sur toutes les fonctions : call.

Alors oui, j'ai bien dit que les fonctions ont des méthodes. En JavaScript, les fonctions sont des objets, elles aussi. Ce sont des instances de Function, spécifiquement. Elles disposent donc de propriétés, qui sont soit des données (name et length, notamment), soit des fonctions (call, apply et bind, spécifiquement). Respire, tout va bien. Ce n’est pas sale, ton code change.

Au lieu de simplement faire :

// Appel classique, ne s’occupant pas de définir `this`
callback(item, index, items)

…elle ferait plutôt ceci :

callback.call(thisArg, item, index, items)

La méthode call d’une fonction permet de l’appeler en précisant d’abord le this à utiliser, puis les arguments éventuels. Note que ceux-ci sont passés individuellement, il faut donc en connaître la liste avant.

Ainsi, une implémentation naïve de forEach pourrait ressembler à ce qui suit. Afin de ne pas t’embrouiller davantage, on n’écrit pas une fonction censée exister comme méthode de tableau (this serait alors le tableau, ça rajoute de la confusion ici), on va lui faire prendre le tableau en premier argument.

function arrayForEach(array, callback, thisArg) {
  // Je hais les boucles numériques et en vrai j’aurais fait un
  // `for…of` sur `array.entries()`, mais ne mélangeons pas tout.
  for (let index = 0, len = array.length; index < len; ++index) {
    callback.call(thisArg, array[index], index, array)
  }
}

Définir this quand on ne connaît pas les arguments

Mais si on veut écrire un code générique, qui forcerait le this quels que soient les arguments ? C’est d’ailleurs ce que fait la méthode bind des fonctions : elle produit une fonction dérivée qui enrobe l’originale, et l’appellera le moment venu avec tous les arguments qui seront passés… et un this prédéfini :

wifeGreet = wife.greet.bind(wife)
wifeGreet('Elliott') // => 'Salut Elliott moi c’est Élodie !' — YAY!

Comment bind s’y prend-elle ?

Elle pourrait utiliser la cousine de call, qui s’appelle apply. Elle applique la fonction à une liste d’arguments (la terminologie vient ici de la programmation fonctionnelle). Cette liste peut être un objet arguments justement, ou tout autre quasi-tableau, c’est-à-dire un objet exposant une propriété length numérique et des propriétés numériques entre 0 et length - 1. L’important, c’est que ce ne sont pas des arguments individuels, on n’a donc pas besoin de connaître leur nombre à l’avance.

Exemple : réimplémenter bind

Ainsi, un équivalent naïf de bind (qui, notamment, ne permettrait pas de préremplir les premiers arguments, ce qu’autorise bind et qu’on appelle de l’application partielle) ressemblerait à ceci :

function bindFx(fx, thisArg) {
  return function() {
    return fx.apply(thisArg, arguments)
  }
}

Supposons qu’on veuille ajouter l’application partielle, en précisant à partir du 3e argument de bindFx le pré-remplissage des premiers arguments de la fonction enrobée :

function bindFx(fx, thisArg) {
  // On prend tous nos arguments à partir de la position 2 (puisque
  // les deux premiers, `fx` et `thisArg`, nous servent à nous)
  const partialArgs = Array.from(arguments).slice(2)
  return function() {
    // On concatène les arguments pré-remplis et ceux qu’on reçoit le
    // moment venu
    const args = partialArgs.concat(Array.from(arguments))
    return fx.apply(thisArg, args)
  }
}

Si ça te vrille un peu le cerveau, ne t’acharne pas : on est sortis du sujet principal, qui est le forçage du this, pour jouer avec arguments et faire de l’application partielle. C’est annexe et ça n’a rien de critique !

Et en ES2015 (ES6) ?

Eh bien même depuis 2015, si on a besoin d’appeler une fonction avec un this précis, on doit toujours en passer par call ou apply. Mais certaines choses ont un peu changé le paysage…

Plus tellement besoin de apply

ES2015 a apporté les syntaxes rest et spread applicables, notamment, dans les signatures et appels de fonctions. Le fait de ne pas savoir à l’avance combien on traite d’arguments n’a donc plus guère d’importance. Notre implémentation naïve de bind pourrait ressembler à ça :

function bindFx(fx, thisArg) {
  return function(...args) {
    return fx.call(thisArg, ...args)
  }
}

L’implémentation avec application partielle serait pas mal simplifiée :

function bindFx(fx, thisArg, ...partialArgs) {
  return function(...moreArgs) {
    return fx.call(thisArg, ...partialArgs, ...moreArgs)
  }
}

Les fonctions fléchées ont un this lexical

Contrairement aux fonctions « traditionnelles », les fonctions fléchées (arrow functions) ne définissent pas au moment de l’appel certaines entrées dans le Function Environment Record qu’on a évoqué tout à l’heure. Ça inclut notamment this et arguments, qui n’y existent pas.

Du coup, dans une fonction fléchée, ces identifiants sont résolus comme n’importe quel autre (par exemple console ou document) : lexicalement, c’est-à-dire en remontant progressivement les portées imbricantes, jusqu’à la portée globale.

Dans une fonction fléchée, le this est celui qui sera rencontré en premier dans cette remontée de portées, donc celui de la fonction englobante dotée d’un this la plus proche. C’est très pratique pour des fonctions de rappel au sein d’une méthode, quand le code rappelé doit continuer à pouvoir utiliser l’objet en question. Par exemple :

const contribFilter = {
  acceptableAuthors: ['Alice', 'Claire', 'Erin', 'Gillian', 'Maxence'],
  process(contribs) {
    return contribs.filter((contrib) =>
      this.acceptableAuthors.includes(contrib.author)
    )
  },
}

En revanche, attention ! Les fonctions fléchées restent des fonctions, sur lesquelles on peut donc appeler call, apply ou bind, sauf que ces appels ne forceront pas le this, sans pour autant lever d’erreur ou même émettre un avertissement ! Gaffe, donc, à du code qui recevrait une fonction pour l’appeler de cette façon, et auquel on passerait une fonction fléchée !

function forceThis(fx) {
  fx.call({ name: 'Mark' })
}

const host = {
  name: 'John',
  run() {
    forceThis(() => console.log(`${this.name} runs`))
  },
}

host.run() // => Logue 'John runs', PAS 'Mark runs'

Dans ce code, même si la fonction fléchée est appelée, dans forceThis, avec un call précisant que this.name devrait être 'Mark', elle ignore superbement cette contrainte et continue à utiliser son this lexical, celui en vigueur dans sa portée conteneur : celle de run().

Puisque celle-ci a été appelée en « sujet-verbe-complément » avec host.run(), this vaut host, et this.name vaut donc 'John'.

Pfew !

Envie d’en savoir plus ?

On a sorti un fabuleux cours vidéo qui traite en profondeur de ce sujet, avec moult schémas, diagrammes animés et exemples de code. Des règles de base au détail des Function Environment Records en passant par les API classiques dont le contrat veut qu’elles forcent le this, sans oublier naturellement call, apply et bind, vous y trouverez tout en 1h30 de vidéo avec transcripts, à voir et revoir à votre rythme !

Si vous êtes curieux·se de JavaScript le langage pur, nous avons aussi une formation phénoménale de 3 jours qui explore en profondeur 100% des aspects avancés et détails du langage et de sa bibliothèque standard. Expressions rationnelles, protocole itérable, promesses, async/await, proxies, générateurs, symboles… rien n’est oublié !

Découvrez notre cours vidéo : JavaScript : this is it ! 🖥

Tout savoir sur le fonctionnement de this en JavaScript, des règles fondamentales aux ajustements des API, en passant par les fonctions fléchées, le binding et bien plus encore…