Lorsque lon transmet des mthodes objets en tant que callbacks, par exemple setTimeout, il y a un problme connu : la perte du this.
Dans ce chapitre nous verrons les faons de rgler a.
La perte du this
Nous avons dj vu des exemples de la perte du this. Une fois quune mthode est passe quelque part sparement de lobjet 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 naffiche pas John pour this.firstName, mais undefined !
Cest car setTimeout a eu la fonction user.sayHi, sparement de lobjet. La dernire ligne pourrait tre rcrite comme :
let f = user.sayHi;
setTimeout(f, 1000); // Perte du contexte d'user
La mthode setTimeout dans le navigateur est un peu spciale : elle dfinit this=window pour lappel la fonction (pour Node.js, this devient un objet timer, mais a na pas dimportance ici). Donc pour this.firstName il essaye de rcuperer window.firstName, qui nexiste pas. Dans dautres cas similaires, this devient gnralement undefined.
Cette tche est plutt commune on veut transmettre une mthode objet quelque part ailleurs (ici au scheduler) o elle sera appele. Comment sassurer quelle sera appele dans le bon contexte ?
Solution 1 : Un wrapper
La solution la plus simple est dutiliser une fonction enveloppe :
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
Maintenant a fonctionne, car elle reoit user depuis un environnement lexical extrieur, et donc les appels la fonction se font normalement.
La mme chose mais en plus court :
setTimeout(() => user.sayHi(), 1000); // Hello, John!
a lair bon, mais une lgre vulnrabilit apparat dans la structure de notre code.
Que se passe t-il si avant le dclenchement de setTimeout (il y une seconde de dlai) 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 narrivera pas
Solution 2 : bind
Les fonctions fournissent une mthode intgre, bind qui permet de corriger this.
La syntaxe basique est :
// Une syntaxe plus complexe arrivera bientot
let boundFunc = func.bind(context);
Le rsultat de func.bind(context) est une objet exotique dans le style dune fonction, qui est appellable comme une fonction et qui transmet lappel func en dfinissant this=context de faon transparente.
En dautres termes, appeller boundFunc quivaut func avec un this corrig.
Par exemple, ici funcUser passe lappel 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 lie de func, avec this=user.
Tous les arguments sont passs loriginale 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 mthode objet :
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// Peut s'excuter sans objet
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
// Mme si la valeur de user change dans 1 seconde
// sayHi utilise la valeur pr-lie, laquelle fait rfrence l'ancien objet user
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
Sur la ligne (*) nous prenons la mthode user.sayHi en nous la lions user. La mthode sayHi est une fonction lie, qui peut tre appele seule ou tre transmise setTimeout a na pas dimportance, le contexte sera le bon.
Ici, nous pouvons voir que les arguments passs 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 mthodes et que nous prvoyons 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. Cest rarement utilis, mais a peut tre pratique.
La syntaxe complte de bind :
let bound = func.bind(context, [arg1], [arg2], ...);
Elle permet de lier le contexte en tant que this et de dmarrer 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 crer 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
Lappel mul.bind(null, 2) crer une nouvelle fonction double qui transmet les appels mul, corrigeant null dans le contexte et 2 comme premier argument. Les arguments sont passs tels quels plus loin.
a sappelle lapplication de fonction partielle nous creons une nouvelle fonction en corrigeant certains paramtres dune fonction existante.
Veuillez noter que nous nutilisons 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 gnralement une fonction partielle ?
Lavantage de faire a est que nous pouvons crer une fonction indpendante avec un nom lisible (double, triple). Nous pouvons les utiliser et ne pas fournir de premier argument chaque fois puisque cest corrig par bind.
Dans dautres cas, les fonctions partielles sont utiles quand nous avons des fonctions vraiment gnriques 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 lutilisateur 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 mthode 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 implmente.
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 mthode 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 rsultat de lappel partial(func[, arg1, arg2...]) est une enveloppe (*) qui appelle func avec :
- Le mme
thisquil rcupre (pouruser.sayNowlappel estuser) - Alors il donne
...argsBoundles arguments provenant de lappel departial("10:00") - Alors il donne
...argsles arguments donns lenveloppe ("Hello")
Alors, cest simple faire avec la spread syntaxe, pas vrai ?
Aussi il y a une implmentation de _.partial prte lemploi dans les librairies lodash.
Rsum
La mthode func.bind(context, ...args) retourne une variante lie de la fonction func qui corrige le contexte de this et des premiers arguments sils sont donns.
Nous appliquons gnralement bind pour corriger this pour une mthode objet, comme a nous pouvons la passer ailleurs. Par exemple, setTimeout.
Quand nous corrigeons certains arguments dune fonction existante, la fonction (moins universelle) en rsultant est dite partiellement applique ou partielle.
Les fonctions partielles sont pratiques quand nous ne voulons pas rpter le mme argument encore et encore. Comme si nous avions une fonction send(from, to), et que from devait tre toujours le mme pour notre tche, nous pourrions rcuprer 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)