Lors du passage de méthodes d'objet en tant que rappels, par exemple à setTimeout
, il existe un problème connu : « perdre this
».
Dans ce chapitre, nous verrons les moyens de résoudre ce problème.
Nous avons déjà vu des exemples de perte this
. Une fois qu'une méthode est passée quelque part séparément de l'objet, this
est perdue.
Voici comment cela peut se produire avec setTimeout
:
laissez l'utilisateur = { prénom: "John", disBonjour() { alert(`Bonjour, ${this.firstName}!`); } } ; setTimeout(user.sayHi, 1000); // Bonjour, indéfini !
Comme nous pouvons le voir, la sortie n'affiche pas « John » comme this.firstName
, mais undefined
!
C'est parce que setTimeout
a obtenu la fonction user.sayHi
, séparément de l'objet. La dernière ligne peut être réécrite comme suit :
soit f = user.sayHi; setTimeout(f, 1000); // perte du contexte utilisateur
La méthode setTimeout
dans le navigateur est un peu spéciale : elle définit this=window
pour l'appel de fonction (pour Node.js, this
devient l'objet timer, mais n'a pas vraiment d'importance ici). Donc, pour this.firstName
il essaie d'obtenir window.firstName
, qui n'existe pas. Dans d'autres cas similaires, this
devient généralement undefined
.
La tâche est assez typique : nous voulons transmettre une méthode objet ailleurs (ici – au planificateur) où elle sera appelée. Comment s’assurer qu’il sera appelé dans le bon contexte ?
La solution la plus simple consiste à utiliser une fonction d'emballage :
laissez l'utilisateur = { prénom: "John", disBonjour() { alert(`Bonjour, ${this.firstName}!`); } } ; setTimeout(fonction() { user.sayHi(); // Bonjour, Jean ! }, 1000);
Maintenant, cela fonctionne, car il reçoit user
de l'environnement lexical externe, puis appelle la méthode normalement.
Le même, mais plus court :
setTimeout(() => user.sayHi(), 1000); // Bonjour, Jean !
Cela semble bien, mais une légère vulnérabilité apparaît dans notre structure de code.
Que se passe-t-il si, avant le déclenchement setTimeout
(il y a un délai d'une seconde !), user
modifie la valeur ? Puis, du coup, il appellera le mauvais objet !
laissez l'utilisateur = { prénom: "John", disBonjour() { alert(`Bonjour, ${this.firstName}!`); } } ; setTimeout(() => user.sayHi(), 1000); // ... la valeur de l'utilisateur change en 1 seconde utilisateur = { sayHi() { alert("Un autre utilisateur dans setTimeout!"); } } ; // Un autre utilisateur dans setTimeout !
La solution suivante garantit qu’une telle chose n’arrivera pas.
Les fonctions fournissent une méthode intégrée de liaison qui permet de résoudre this
.
La syntaxe de base est la suivante :
// une syntaxe plus complexe viendra un peu plus tard laissezboundFunc = func.bind(context);
Le résultat de func.bind(context)
est un « objet exotique » spécial semblable à une fonction, qui peut être appelé en tant que fonction et transmet de manière transparente l'appel au paramètre func
this=context
.
En d'autres termes, boundFunc
est comme func
avec this
corrigé.
Par exemple, ici funcUser
passe un appel à func
avec this=user
:
laissez l'utilisateur = { prénom: "Jean" } ; fonction fonction() { alert(this.firstName); } laissez funcUser = func.bind(user); funcUser(); // John
Ici func.bind(user)
comme « variante liée » de func
, avec this=user
corrigé.
Tous les arguments sont transmis à la func
d'origine « tels quels », par exemple :
laissez l'utilisateur = { prénom: "John" } ; fonction func(phrase) { alert(phrase + ', ' + this.firstName); } // lie ceci à l'utilisateur laissez funcUser = func.bind(user); funcUser("Bonjour"); // Bonjour, John (l'argument "Bonjour" est passé, et this=user)
Essayons maintenant avec une méthode objet :
laissez l'utilisateur = { prénom: "John", disBonjour() { alert(`Bonjour, ${this.firstName}!`); } } ; laissez sayHi = user.sayHi.bind(user); // (*) // peut l'exécuter sans objet disBonjour(); // Bonjour, Jean ! setTimeout(sayHi, 1000); // Bonjour, Jean ! // même si la valeur de l'utilisateur change en 1 seconde // sayHi utilise la valeur pré-liée qui fait référence à l'ancien objet utilisateur utilisateur = { sayHi() { alert("Un autre utilisateur dans setTimeout!"); } } ;
Dans la ligne (*)
nous prenons la méthode user.sayHi
et la lions à user
. Le sayHi
est une fonction « liée », qui peut être appelée seule ou passée à setTimeout
– peu importe, le contexte sera bon.
Ici, nous pouvons voir que les arguments sont passés « tels quels », mais this
est corrigé par bind
:
laissez l'utilisateur = { prénom: "John", dire(phrase) { alert(`${phrase}, ${this.firstName}!`); } } ; disons = user.say.bind(user); say("Bonjour"); // Bonjour, Jean ! (L'argument "Bonjour" est passé pour dire) dire("Au revoir"); // Au revoir, John ! ("Au revoir" est passé pour dire)
Méthode pratique : bindAll
Si un objet possède de nombreuses méthodes et que nous prévoyons de le transmettre activement, nous pourrions alors les lier toutes dans une boucle :
pour (laisser la clé dans l'utilisateur) { if (type d'utilisateur [clé] == 'fonction') { utilisateur[clé] = utilisateur[clé].bind(utilisateur); } }
Les bibliothèques JavaScript fournissent également des fonctions pour une liaison de masse pratique, par exemple _.bindAll(object, methodNames) dans lodash.
Jusqu'à présent, nous n'avons parlé que de lier this
. Allons un peu plus loin.
Nous pouvons lier non seulement this
, mais aussi des arguments. C'est rarement fait, mais cela peut parfois s'avérer utile.
La syntaxe complète de bind
:
letbound = func.bind(context, [arg1], [arg2], ...);
Il permet de lier le contexte en tant que this
et les arguments de départ de la fonction.
Par exemple, nous avons une fonction de multiplication mul(a, b)
:
fonction mul(a, b) { renvoyer un * b ; }
Utilisons bind
pour créer une fonction double
sur sa base :
fonction mul(a, b) { renvoyer un * b ; } laissez double = mul.bind(null, 2); alerte( double(3) ); // = mul(2, 3) = 6 alerte( double(4) ); // = mul(2, 4) = 8 alerte( double(5) ); // = mul(2, 5) = 10
L'appel à mul.bind(null, 2)
crée une nouvelle fonction double
qui transmet les appels à mul
, en fixant null
comme contexte et 2
comme premier argument. Les autres arguments sont transmis « tels quels ».
C'est ce qu'on appelle l'application partielle d'une fonction : nous créons une nouvelle fonction en corrigeant certains paramètres de la fonction existante.
Veuillez noter que nous ne this
utilisons pas ici. Mais bind
l’exige, nous devons donc mettre quelque chose comme null
.
La fonction triple
dans le code ci-dessous triple la valeur :
fonction mul(a, b) { renvoyer un * b ; } soit triple = mul.bind(null, 3); alerte( triple(3) ); // = mul(3, 3) = 9 alerte( triple(4) ); // = mul(3, 4) = 12 alerte( triple(5) ); // = mul(3, 5) = 15
Pourquoi créons-nous généralement une fonction partielle ?
L'avantage est que l'on peut créer une fonction indépendante avec un nom lisible ( double
, triple
). Nous pouvons l'utiliser et ne pas fournir le premier argument à chaque fois car il est corrigé avec bind
.
Dans d’autres cas, l’application partielle est utile lorsque nous avons une fonction très générique et que nous souhaitons une variante moins universelle pour plus de commodité.
Par exemple, nous avons une fonction send(from, to, text)
. Ensuite, à l'intérieur d'un objet user
, nous souhaiterons peut-être en utiliser une variante partielle : sendTo(to, text)
qui envoie depuis l'utilisateur actuel.
Et si nous souhaitons corriger certains arguments, mais pas le this
? Par exemple, pour une méthode objet.
La bind
native ne le permet pas. Nous ne pouvons pas simplement omettre le contexte et passer aux arguments.
Heureusement, une fonction partial
pour lier uniquement les arguments peut être facilement implémentée.
Comme ça:
fonction partielle (func, ...argsBound) { fonction de retour(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // Utilisation : laissez l'utilisateur = { prénom: "John", dire (heure, phrase) { alert(`[${time}] ${this.firstName} : ${phrase}!`); } } ; // ajoute une méthode partielle à temps fixe user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Bonjour"); // Quelque chose comme : // [10:00] John : Bonjour !
Le résultat de l'appel partial(func[, arg1, arg2...])
est un wrapper (*)
qui appelle func
avec :
C'est pareil que this
(pour user.sayNow
appelez-le user
)
Puis lui donne ...argsBound
– arguments de l'appel partial
( "10:00"
)
Puis lui donne ...args
– arguments donnés au wrapper ( "Hello"
)
C'est si facile à faire avec la syntaxe spread, n'est-ce pas ?
Il existe également une implémentation _.partial prête à partir de la bibliothèque lodash.
La méthode func.bind(context, ...args)
renvoie une « variante liée » de la fonction func
qui corrige le contexte this
et first arguments s'ils sont donnés.
Habituellement, nous appliquons bind
pour résoudre this
pour une méthode objet, afin de pouvoir la transmettre quelque part. Par exemple, pour setTimeout
.
Lorsque nous corrigeons certains arguments d'une fonction existante, la fonction résultante (moins universelle) est appelée partiellement appliquée ou partielle .
Les partiels sont pratiques lorsque nous ne voulons pas répéter encore et encore le même argument. Comme si nous avions une fonction send(from, to)
et que from
devait toujours être le même pour notre tâche, nous pouvons en obtenir un partiel et continuer.
importance : 5
Quel sera le résultat ?
fonction f() { alerter(ceci); // ? } laissez l'utilisateur = { g : f.bind (null) } ; utilisateur.g();
La réponse : null
.
fonction f() { alerter(ceci); // nul } laissez l'utilisateur = { g : f.bind (null) } ; utilisateur.g();
Le contexte d'une fonction liée est fixé en dur. Il n'y a tout simplement aucun moyen de le modifier davantage.
Ainsi, même pendant que nous exécutons user.g()
, la fonction d'origine est appelée avec this=null
.
importance : 5
Pouvons-nous changer this
par une liaison supplémentaire ?
Quel sera le résultat ?
fonction f() { alert(ce.nom); } f = f.bind( {nom : "John"} ).bind( {nom : "Ann" } ); f();
La réponse : Jean .
fonction f() { alert(ce.nom); } f = f.bind( {nom : "John"} ).bind( {nom : "Pete"} ); f(); // John
L'objet fonction lié exotique renvoyé par f.bind(...)
se souvient du contexte (et des arguments s'ils sont fournis) uniquement au moment de la création.
Une fonction ne peut pas être liée à nouveau.
importance : 5
Il y a une valeur dans la propriété d'une fonction. Est-ce que cela changera après bind
? Pourquoi, ou pourquoi pas ?
fonction direSalut() { alert( this.name ); } sayHi.test = 5; laissez lié = sayHi.bind ({ prénom : "Jean" }); alert( lié.test ); // quel sera le résultat ? pourquoi ?
La réponse : undefined
.
Le résultat de bind
est un autre objet. Il n'a pas la propriété test
.
importance : 5
L'appel à askPassword()
dans le code ci-dessous doit vérifier le mot de passe, puis appeler user.loginOk/loginFail
en fonction de la réponse.
Mais cela conduit à une erreur. Pourquoi?
Corrigez la ligne en surbrillance pour que tout commence à fonctionner correctement (les autres lignes ne doivent pas être modifiées).
function requestPassword (ok, échec) { let password = prompt("Mot de passe?", ''); if (mot de passe == "rockstar") ok(); sinon fail(); } laissez l'utilisateur = { nom : « Jean », connexionOk() { alert(`${this.name} connecté`); }, échec de connexion() { alert(`${this.name} n'a pas réussi à se connecter`); }, } ; AskPassword(user.loginOk, user.loginFail);
L'erreur se produit car askPassword
obtient les fonctions loginOk/loginFail
sans l'objet.
Lorsqu'il les appelle, ils supposent naturellement this=undefined
.
bind
le contexte :
function requestPassword (ok, échec) { let password = prompt("Mot de passe?", ''); if (mot de passe == "rockstar") ok(); sinon fail(); } laissez l'utilisateur = { nom : « Jean », connexionOk() { alert(`${this.name} connecté`); }, échec de connexion() { alert(`${this.name} n'a pas réussi à se connecter`); }, } ; AskPassword(user.loginOk.bind(user), user.loginFail.bind(user));
Maintenant ça marche.
Une solution alternative pourrait être :
//... AskPassword(() => user.loginOk(), () => user.loginFail());
Habituellement, cela fonctionne aussi et a l'air bien.
C'est un peu moins fiable cependant dans des situations plus complexes où la variable user
peut changer après l'appel askPassword
, mais avant que le visiteur ne réponde et n'appelle () => user.loginOk()
.
importance : 5
La tâche est une variante un peu plus complexe de Corriger une fonction qui perd "ceci".
L'objet user
a été modifié. Désormais, au lieu de deux fonctions loginOk/loginFail
, il a une seule fonction user.login(true/false)
.
Que devons-nous transmettre askPassword
dans le code ci-dessous, afin qu'il appelle user.login(true)
comme ok
et user.login(false)
comme fail
?
function requestPassword (ok, échec) { let password = prompt("Mot de passe?", ''); if (mot de passe == "rockstar") ok(); sinon fail(); } laissez l'utilisateur = { nom : « Jean », connexion (résultat) { alert( this.name + (result ? 'connecté' : 'échec de la connexion') ); } } ; demanderMot de passe(?, ?); // ?
Vos modifications ne doivent modifier que le fragment en surbrillance.
Soit utilisez une fonction wrapper, une flèche pour être concis :
AskPassword(() => user.login(true), () => user.login(false));
Maintenant, il récupère user
des variables externes et l'exécute normalement.
Ou créez une fonction partielle à partir de user.login
qui utilise user
comme contexte et possède le premier argument correct :
AskPassword(user.login.bind(user, true), user.login.bind(user, false));