Lorsque lâon transmet des méthodes objets en tant que callbacks, par exemple à setTimeout, il y a un problème connu : âla perte du thisâ.
Dans ce chapitre nous verrons les façons de régler ça.
La perte du âthisâ
Nous avons déjà vu des exemples de la perte du this. Une fois quâune méthode est passée quelque part séparement de lâobjet â this est perdu.
Voici comment cela pourrait arriver avec setTimeout :
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
Comme nous pouvons le voir, la sortie nâaffiche pas âJohnâ pour this.firstName, mais undefined !
Câest car setTimeout a eu la fonction user.sayHi, séparement de lâobjet. La dernière ligne pourrait être réécrite comme :
let f = user.sayHi;
setTimeout(f, 1000); // Perte du contexte d'user
La méthode setTimeout dans le navigateur est un peu spéciale : elle définit this=window pour lâappel à la fonction (pour Node.js, this devient un objet âtimerâ, mais ça nâa pas dâimportance ici). Donc pour this.firstName il essaye de récuperer window.firstName, qui nâexiste pas. Dans dâautres cas similaires, this devient généralement undefined.
Cette tâche est plutôt commune â on veut transmettre une méthode objet quelque part ailleurs (ici â au scheduler) où elle sera appelée. Comment sâassurer quâelle sera appelée dans le bon contexte ?
Solution 1 : Un wrapper
La solution la plus simple est dâutiliser une fonction enveloppée :
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
Maintenant ça fonctionne, car elle reçoit user depuis un environnement lexical extérieur, et donc les appels à la fonction se font normalement.
La même chose mais en plus court :
setTimeout(() => user.sayHi(), 1000); // Hello, John!
Ãa à lâair bon, mais une légère vulnérabilité apparaît dans la structure de notre code.
Que se passe t-il si avant le déclenchement de setTimeout (il y une seconde de délai) user changeait de valeur ? Alors, soudainement, ça appelera le mauvais objet !
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...La valeur d'user dans 1 seconde
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
// Un autre user est dans le setTimeout !
La prochaine solution garantit que ce genre de chose nâarrivera pas
Solution 2 : âbindâ
Les fonctions fournissent une méthode intégrée, bind qui permet de corriger this.
La syntaxe basique est :
// Une syntaxe plus complexe arrivera bientot
let boundFunc = func.bind(context);
Le résultat de func.bind(context) est une âobjet exotiqueâ dans le style dâune fonction, qui est appellable comme une fonction et qui transmet lâappel à func en définissant this=context de façon transparente.
En dâautres termes, appeller boundFunc équivaut à func avec un this corrigé.
Par exemple, ici funcUser passe lâappel à this tel que this=user :
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
Ici func.bind(user) en tant âvariante liéeâ de func, avec this=user.
Tous les arguments sont passés à lâoriginale func âtels quelsâ, par exemple :
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
// Lie this à user
let funcUser = func.bind(user);
funcUser("Hello"); // Hello, John (l'argument "Hello" est passé, et this=user)
Maintenant essayons avec une méthode objet :
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// Peut s'exécuter sans objet
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
// Mème si la valeur de user change dans 1 seconde
// sayHi utilise la valeur pré-liée, laquelle fait référence à l'ancien objet user
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
Sur la ligne (*) nous prenons la méthode user.sayHi en nous la lions à user. La méthode sayHi est une fonction âliéeâ, qui peut être appelée seule ou être transmise à setTimeout â ça nâa pas dâimportance, le contexte sera le bon.
Ici, nous pouvons voir que les arguments passés âtels quelsâ, seulement this est corrigé par bind :
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John! (l'argument "Hello" est passé à say)
say("Bye"); // Bye, John! (l'argument "Bye" est passé à say)
bindAllSi un objet a plusieurs méthodes et que nous prévoyons de le transmettre plusieurs fois, alors on pourrait toutes les lier dans une boucle :
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
Les librairies JavaScript fournissent aussi des fonctions pratiques pour les liaisons de masse, e.g. _.bindAll(object, methodNames) avec lodash.
Les fonctions partielles
Jusquâà maintenant nous avons parlé uniquement de lier this. Allons plus loin.
Nous pouvons lier this, mais aussi des arguments. Câest rarement utilisé, mais ça peut être pratique.
La syntaxe complète de bind :
let bound = func.bind(context, [arg1], [arg2], ...);
Elle permet de lier le contexte en tant que this et de démarrer les arguments de la fonction.
Par exemple, nous avons une fonction de multiplication mul(a, b) :
function mul(a, b) {
return a * b;
}
Utilisons bind pour créer une fonction double sur cette base :
function mul(a, b) {
return a * b;
}
let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
Lâappel à mul.bind(null, 2) créer une nouvelle fonction double qui transmet les appels à mul, corrigeant null dans le contexte et 2 comme premier argument. Les arguments sont passés âtels quelsâ plus loin.
Ãa sâappelle lâapplication de fonction partielle â nous créeons une nouvelle fonction en corrigeant certains paramètres dâune fonction existante.
Veuillez noter que nous nâutilisons actuellement pas this ici. Mais bind en a besoin, donc nous devrions mettre quelque chose dedans comme null.
La fonction triple dans le code ci-dessous triple la valeur :
function mul(a, b) {
return a * b;
}
let triple = mul.bind(null, 3);
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15
Pourquoi faisons nous généralement une fonction partielle ?
Lâavantage de faire ça est que nous pouvons créer une fonction indépendante avec un nom lisible (double, triple). Nous pouvons les utiliser et ne pas fournir de premier argument à chaque fois puisque câest corrigé par bind.
Dans dâautres cas, les fonctions partielles sont utiles quand nous avons des fonctions vraiment génériques et que nous voulons une variante moins universelle pour des raisons pratiques.
Par exemple, nous avons une fonction send(from, to, text). Alors, dans un objet user nous pourrions vouloir en utiliser une variante partielle : sendTo(to, text) qui envoie depuis lâutilisateur actuel.
Aller dans les partielles sans contexte
Que se passerait t-il si nous voulions corriger certains arguments, mais pas le contexte this ?
Par exemple, pour une méthode objet.
La fonction bind native ne permet pas ça. Nous ne pouvons pas juste omettre le contexte et aller directement aux arguments.
Heureusement, une fonction partial pour lier seulement les arguments peut être facilement implémentée.
Comme ça :
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// Utilisation :
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// Ajoute une méthode partielle avec time corrigé
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// Quelque chose du genre :
// [10:00] John: Hello!
Le résultat de lâappel partial(func[, arg1, arg2...]) est une enveloppe (*) qui appelle func avec :
- Le même
thisquâil récupère (pouruser.sayNowlâappel estuser) - Alors il donne
...argsBoundâ les arguments provenant de lâappel departial("10:00") - Alors il donne
...argsâ les arguments donnés à lâenveloppe ("Hello")
Alors, câest simple à faire avec la spread syntaxe, pas vrai ?
Aussi il y a une implémentation de _.partial prête à lâemploi dans les librairies lodash.
Résumé
La méthode func.bind(context, ...args) retourne une âvariante liéeâ de la fonction func qui corrige le contexte de this et des premiers arguments sâils sont donnés.
Nous appliquons généralement bind pour corriger this pour une méthode objet, comme ça nous pouvons la passer ailleurs. Par exemple, à setTimeout.
Quand nous corrigeons certains arguments dâune fonction existante, la fonction (moins universelle) en résultant est dite partiellement appliquée ou partielle.
Les fonctions partielles sont pratiques quand nous ne voulons pas répéter le même argument encore et encore. Comme si nous avions une fonction send(from, to), et que from devait être toujours le même pour notre tâche, nous pourrions récupérer une partielle et continuer.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)