Apprenez en lisant dans l'ordre

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)
Le même total, mais pas le même this
Corps de total()lit this.itemscart.total()(cart à gauche)calcTotal()(en variable)total()(déstructurée)this vautcartthis vautundefinedthis vautundefinedRenvoie 4980Lève uneTypeErrorLève uneTypeError
Les trois appels exécutent le même corps de total. this ne vaut cart que dans l'appel où cart est à gauche du point.

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().

Au comptoir d'un pressing, tu réutilises une méthode qui produit une ligne d'état pour chaque vêtement déposé. LaundryItem, shirt et coat sont déjà déclarés.

① Appelle cette méthode sur shirt et affiche sa ligne d'état.

② Place la même méthode, prise sur shirt, dans la propriété line de coat, puis appelle-la sur coat et affiche le résultat.

③ Extrais la même méthode de shirt par déstructuration et appelle-la seule.

(Si le code s'exécute correctement, une explication s'affiche.)

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
La valeur de retour de bind et un sous-total ajouté après coup
cart.total.bind(cart)Nouvelle fonction,this fixé à cartPas cart.total=== donne falseboundTotal()vaut 4980cart.items.push(500)Rappel deboundTotal()Lit les items decart à ce momentRenvoie 5480
bind ne modifie pas cart.total : il renvoie une nouvelle fonction. Ce qui est fixé, c'est cart lui-même, pas le montant du total.

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.

Tu extrais la méthode de facturation d'un abonnement. Subscription, coffee et tea sont déjà déclarés.

① Place bill de coffee dans billCoffee, avec this fixé à coffee.

② Affiche le montant pour 3 mois avec billCoffee.

③ Crée billTea pour tea de la même façon et affiche le montant pour 6 mois.

④ Convertis chaque élément de [1, 12] avec billCoffee, relie les résultats par « / » et affiche-les.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

É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
D'où addLike tient son this
Corps de class LikeButton
Initialisation des champs (à chaque new)
  • likes = 0 — placé sur button
  • this dans la valeur initiale de label — le button en cours de création (comme dans constructor)
Dans addLike = () => { ... }
  • Pas de this propre
  • this.likes — lit la valeur de button dans la portée englobante
  • Même appelée par onPress(), this vaut button
Dans addLikeMethod() { ... }
  • this — ce qui est à gauche du point sur la ligne d'appel
  • button pour button.addLikeMethod()
  • undefined si tu l'extrais et l'appelles
Une fonction fléchée utilise le this de l'initialisation des champs. Seul addLikeMethod voit son this changer selon la ligne d'appel.

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() { ... }.

Tu construis une table qui associe chaque bouton de quantité à la fonction appelée quand on appuie dessus. QuantityStepper, mug, buttons et pressed sont déjà déclarés.

① Définis increase, qui ajoute 1 à la quantité et la renvoie, de sorte qu'elle marche même une fois extraite.

② Écris decrease, qui retire 1, de la même façon.

③ Appelle les fonctions de buttons dans l'ordre de pressed et affiche la quantité.

④ Affiche le résultat de buttons.oldPlus() et la quantité de mug.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat

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
this quand setTimeout appelle la fonction
setTimeout(this.showMessage)AppelleshowMessage100 ms plus tardthis reçoitwindowMerci pour votrecommande,undefinedsetTimeout(() =>this.showMessage())100 ms plus tard,appelle la fléchéethis denotifyWithArrow= noticeMerci pour votrecommande, Alice
Dans les deux cas, c'est setTimeout qui fait l'appel 100 ms plus tard. Dans la fonction fléchée, tu peux appeler la méthode avec notice à gauche du point.

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.

AppelValeur de thisRésultat ici
cart.total(), cart avant le pointcart, avant le pointRenvoie 4980
Extraite, appelée par calcTotal()undefinedLève une TypeError
Retour de cart.total.bind(cart)cart, passé à bindRenvoie 4980
Champ fléché extrait, onPress()button, qui porte le champLe nombre de J'aime augmente
setTimeout(this.showMessage, 100)window, dans un navigateurAffiche « …, undefined »
setTimeout(() => this.showMessage(), 100)this de la méthode externeAffiche « …, 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.

À la réception d'un hôtel, tu affiches 100 ms plus tard un message indiquant que la chambre est prête. RoomGuide, alice, lea et delay sont déjà déclarés.

① Passe alice.announce telle quelle à setTimeout.

② Fixe le this de lea.announce avant de la passer.

③ Passe alice.announce enveloppée dans une fonction fléchée.

④ Affiche « Accueil terminé » et regarde à quel moment ce message apparaît par rapport aux messages de ① à ③.

Éditeur JavaScript / TypeScript

Exécuter le code pour voir le résultat
QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Après const calc = cart.total;, que vaut this dans total quand tu appelles calc() ?

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 ?