Appeler une fonction JavaScript avec un this explicite
Par Christophe Porteneuve • Publié le 25 février 2020
• 7 min
This page is also available in English.
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 » :
- Sujet : un objet est utilisé au début de l’expression
- 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. - 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 :
- Le sujet :
wife
- Le verbe :
greet
- 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é !