Question 1Après const calc = cart.total;, que vaut this dans total quand tu appelles calc() ?
La liaison de this — un this qui dépend de l'appel, et comment le fixer
Comment this est déterminé quand tu extrais une méthode ou la passes en callback : la TypeError, bind, les champs en fonction fléchée et setTimeout.
Il arrive qu'on passe une méthode qui calcule le total d'un panier à un autre bout de code, qui l'appellera plus tard. Une méthode qui fonctionne très bien sous la forme cart.total() peut lever une erreur dès que tu la mets dans une variable ou que tu la passes à setTimeout, parce que this change.
Cet article traite de la liaison de this, qui dépend de la façon dont la fonction est appelée, et des façons de fixer this avec bind ou une fonction fléchée.
Une méthode extraite ne marche plus — this dans les méthodes
Tu veux réutiliser cart.total(), qui calcule le total du panier, pour le montant affiché sur la page, et tu la mets donc dans une variable avec const calcTotal = cart.total;. Mais quand tu appelles calcTotal(), une erreur survient avant le moindre calcul, alors que le contenu du panier n'a pas changé.
La valeur de this dans une méthode est déterminée par la ligne qui l'appelle, et non par l'endroit où la méthode est écrite : c'est ce qu'on appelle la liaison de this (le fait qu'une valeur soit associée à this). Si tu lis cart.total sans (), tu obtiens seulement la fonction ; cart ne vient pas avec.
Quand tu appelles calcTotal() sans rien à gauche du point, this vaut undefined dans une classe.
class Cart {
constructor(items) {
this.items = items; // Sous-total de chaque article
}
total() {
return this.items.reduce((sum, price) => sum + price, 0);
}
}
const cart = new Cart([1980, 3000]);
// Appel avec cart à gauche du point
console.log(cart.total()); // 4980
// La mettre dans une variable / l'extraire par déstructuration
const calcTotal = cart.total;
const { total } = cart;
console.log(typeof calcTotal); // function
// calcTotal(); // TypeError: Cannot read properties of undefined (reading 'items')
// total(); // TypeError (même raison)
Les colonnes du milieu et de droite lèvent toutes les deux TypeError: Cannot read properties of undefined (reading 'items'). La déstructuration const { total } = cart; copie elle aussi une seule fonction dans une variable : total() échoue donc pour la même raison que calcTotal().
Garder this dans une variable — la méthode bind
Tu veux maintenant confier le calcul du total, sous forme de fonction, au code qui affiche le montant. Comme on l'a vu dans la section précédente, mettre cart.total dans une variable laisse cart de côté : une TypeError survient dès que le code qui l'a reçue l'appelle.
bind (une méthode que possède toute fonction, et qui renvoie une nouvelle fonction dont this est fixé à la valeur passée en argument) s'écrit cart.total.bind(cart). La fonction renvoyée garde cart comme this, même si tu l'appelles sans rien à gauche du point.
bind ne touche pas à la méthode d'origine : c'est donc la valeur de retour que tu dois appeler ou transmettre.
class Cart {
constructor(items) { this.items = items; } // Sous-total de chaque article
total() { return this.items.reduce((sum, price) => sum + price, 0); }
}
const cart = new Cart([1980, 3000]);
// Une méthode simplement extraite reçoit undefined comme this à l'appel
const unbound = cart.total;
// unbound(); // TypeError
// bind renvoie une nouvelle fonction dont this est fixé à cart
const boundTotal = cart.total.bind(cart);
console.log(boundTotal()); // 4980
console.log(boundTotal === cart.total); // false
// Ce qui est fixé, c'est cart lui-même : les sous-totaux ajoutés ensuite sont lus aussi
cart.items.push(500);
console.log(boundTotal()); // 5480
total ne prend pas d'argument, mais si tu fixes une méthode qui en prend, les arguments passés à la valeur de retour arrivent tels quels à la méthode d'origine. Seul this est fixé : tu peux donc changer d'arguments à chaque appel.
Écrire des méthodes qui marchent une fois extraites — les fonctions fléchées
Sur une page d'articles, chaque bouton « J'aime » reçoit la même méthode comme fonction à appeler lors d'un clic. S'il faut ajouter bind à chaque fois, il suffit de l'oublier une seule fois pour que ce bouton-là lève une TypeError.
Un champ en fonction fléchée (un champ de classe dont la valeur initiale est une fonction fléchée) est créé séparément pour chaque instance, à chaque new. Dans l'expression de la valeur initiale, this désigne la nouvelle instance, comme dans constructor, et la fonction fléchée, qui n'a pas de this propre, utilise ce this.
Tu peux donc l'extraire et l'appeler sans bind.
class LikeButton {
likes = 0;
label = `J'aime : ${this.likes}`; // this dans une valeur initiale = l'instance en cours de création
// Méthode classique : this dépend de la ligne d'appel
addLikeMethod() { this.likes += 1; return this.likes; }
// Champ en fonction fléchée : utilise le même this que les valeurs initiales
addLike = () => { this.likes += 1; return this.likes; };
}
const button = new LikeButton();
console.log(button.label); // J'aime : 0
// L'extraire et l'appeler sans bind
const onPress = button.addLike;
console.log(onPress()); // 1
console.log(onPress()); // 2
console.log(Object.keys(button).join(", ")); // likes, label, addLike
// Une méthode classique extraite ne marche plus
const onPressMethod = button.addLikeMethod;
// onPressMethod(); // TypeError
likes = 0— placé surbuttonthisdans la valeur initiale delabel— lebuttonen cours de création (comme dans constructor)
- Pas de
thispropre this.likes— lit la valeur debuttondans la portée englobante- Même appelée par
onPress(),thisvautbutton
this— ce qui est à gauche du point sur la ligne d'appelbuttonpourbutton.addLikeMethod()undefinedsi tu l'extrais et l'appelles
addLike apparaît dans Object.keys(button) parce qu'un champ en fonction fléchée est placé sur l'instance elle-même, comme likes ; addLikeMethod, lui, n'apparaît pas. Cela veut aussi dire que si tu crées 100 instances, 100 fonctions addLike sont créées.
Dans un littéral objet, une fonction fléchée ne reçoit pas l'objet comme this
Avec const counter = { add: () => { this.likes += 1 } };, this ne désigne pas counter quand tu appelles counter.add(). Un littéral objet ne définit pas this : la fonction fléchée utilise le this extérieur. Écris plutôt add() { ... }.
Passer une méthode à du code qui l'appelle plus tard — setTimeout
Tu veux afficher un message de remerciement avec le nom du client 100 ms après la validation d'une commande. Si tu écris setTimeout(this.showMessage, 100) dans une méthode, le message s'affiche bien 100 ms plus tard, mais le nom du client sort sous la forme undefined.
setTimeout appelle lui-même la fonction reçue 100 ms plus tard et, dans un navigateur, this vaut alors window (window est un objet fourni par le navigateur, un par page). Si tu l'enveloppes dans une fonction fléchée, le this de () => this.showMessage() est celui de la méthode englobante : la méthode est donc appelée sur l'instance.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
class OrderNotice {
constructor(customerName) { this.customerName = customerName; }
showMessage() { console.log(`Merci pour votre commande, ${this.customerName}`); }
// Passer la méthode telle quelle. C'est setTimeout qui l'appelle 100 ms plus tard
notifyDirect() { setTimeout(this.showMessage, 100); }
// L'envelopper dans une fonction fléchée / la fixer avec bind avant de la passer
notifyWithArrow() { setTimeout(() => this.showMessage(), 100); }
notifyWithBind() { setTimeout(this.showMessage.bind(this), 100); }
}
const notice = new OrderNotice("Alice");
notice.notifyDirect(); // Merci pour votre commande, undefined
notice.notifyWithArrow(); // Merci pour votre commande, Alice
notice.notifyWithBind(); // Merci pour votre commande, Alice
await delay(300); // Sur cette page, attendre que tout l'affichage soit sorti
La ligne du haut ne lève pas de TypeError : contrairement à calcTotal(), this n'y vaut pas undefined mais window. Comme window n'a pas de customerName, c'est undefined qui s'affiche. Le tableau ci-dessous récapitule la valeur de this pour chaque façon d'appeler vue dans cet article.
| Appel | Valeur de this | Résultat ici |
|---|---|---|
| cart.total(), cart avant le point | cart, avant le point | Renvoie 4980 |
| Extraite, appelée par calcTotal() | undefined | Lève une TypeError |
| Retour de cart.total.bind(cart) | cart, passé à bind | Renvoie 4980 |
| Champ fléché extrait, onPress() | button, qui porte le champ | Le nombre de J'aime augmente |
| setTimeout(this.showMessage, 100) | window, dans un navigateur | Affiche « …, undefined » |
| setTimeout(() => this.showMessage(), 100) | this de la méthode externe | Affiche « …, Alice » |
Envelopper dans une function ne rétablit pas this
Si tu l'enveloppes sous la forme setTimeout(function () { this.showMessage(); }, 100), le this intérieur vaut window, et une TypeError est levée. La console de cette page n'affiche même pas cette erreur : tu ne verras « Uncaught TypeError » que dans les outils de développement du navigateur. Enveloppe-la plutôt dans une fonction fléchée.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Si tu écris seulement cart.total.bind(cart);, puis que tu mets cart.total dans une variable et l'appelles, que se passe-t-il ?
Question 3Quand tu passes une méthode à setTimeout depuis une autre méthode, quelle façon de faire ne garde PAS l'instance comme this ?