Při předávání objektových metod jako zpětných volání, například do setTimeout
, existuje známý problém:„ztráta this
".
V této kapitole uvidíme způsoby, jak to opravit.
Ztráta „toto“
Už jsme viděli příklady ztráty this
. Jakmile je metoda předána někde odděleně od objektu – this
je ztraceno.
Zde je návod, jak se to může stát s setTimeout
:
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
Jak vidíme, výstup neukazuje „John“ jako this.firstName
, ale undefined
!
To proto, že setTimeout
dostal funkci user.sayHi
, odděleně od objektu. Poslední řádek lze přepsat jako:
let f = user.sayHi;
setTimeout(f, 1000); // lost user context
Metoda setTimeout
in-browser je trochu speciální:nastavuje this=window
pro volání funkce (pro Node.js, this
se stane objektem časovače, ale zde na tom opravdu nezáleží). Tedy pro this.firstName
pokusí se získat window.firstName
, který neexistuje. V jiných podobných případech obvykle this
se změní na undefined
.
Úloha je zcela typická – chceme objektovou metodu předat jinam (zde – plánovači), kde bude volána. Jak zajistit, že bude volána ve správném kontextu?
Řešení 1:obal
Nejjednodušším řešením je použít funkci zalamování:
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
Nyní to funguje, protože přijímá user
z vnějšího lexikálního prostředí a poté zavolá metodu normálně.
Totéž, ale kratší:
setTimeout(() => user.sayHi(), 1000); // Hello, John!
Vypadá dobře, ale v naší struktuře kódu se objevuje mírná chyba zabezpečení.
Co když před setTimeout
spouští (je zde jednosekundové zpoždění!) user
změní hodnotu? Pak najednou zavolá nesprávný objekt!
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...the value of user changes within 1 second
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
// Another user in setTimeout!
Další řešení zaručuje, že se taková věc nestane.
Řešení 2:svázat
Funkce poskytují vestavěnou vazbu metody, která umožňuje opravit this
.
Základní syntaxe je:
// more complex syntax will come a little later
let boundFunc = func.bind(context);
Výsledek func.bind(context)
je speciální funkce podobný „exotickému objektu“, který lze volat jako funkci a transparentně předává volání func
nastavení this=context
.
Jinými slovy, volání boundFunc
je jako func
s pevným this
.
Například zde funcUser
předá volání na func
s this=user
:
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
Zde func.bind(user)
jako „vázaná varianta“ func
s pevnou hodnotou this=user
.
Všechny argumenty jsou předány původnímu func
„jak je“, například:
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
// bind this to user
let funcUser = func.bind(user);
funcUser("Hello"); // Hello, John (argument "Hello" is passed, and this=user)
Nyní to zkusíme pomocí objektové metody:
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// can run it without an object
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
// even if the value of user changes within 1 second
// sayHi uses the pre-bound value which is reference to the old user object
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
Na řádku (*)
použijeme metodu user.sayHi
a svázat jej s user
. sayHi
je „vázaná“ funkce, kterou lze volat samostatně nebo předat setTimeout
– na tom nezáleží, kontext bude správný.
Zde vidíme, že argumenty jsou předávány „tak jak jsou“, pouze this
je opraveno bind
:
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John! ("Hello" argument is passed to say)
say("Bye"); // Bye, John! ("Bye" is passed to say)
Pohodlná metoda:bindAll
Pokud má objekt mnoho metod a my ho plánujeme aktivně předávat, můžeme je všechny svázat do smyčky:
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
Knihovny JavaScriptu také poskytují funkce pro pohodlnou hromadnou vazbu, např. _.bindAll(object, methodNames) v lodash.
Dílčí funkce
Až dosud jsme mluvili pouze o vazbě this
. Pojďme to udělat o krok dále.
Dokážeme svázat nejen this
, ale také argumenty. To se dělá jen zřídka, ale někdy se to může hodit.
Úplná syntaxe bind
:
let bound = func.bind(context, [arg1], [arg2], ...);
Umožňuje svázat kontext jako this
a počáteční argumenty funkce.
Máme například funkci násobení mul(a, b)
:
function mul(a, b) {
return a * b;
}
Použijme bind
k vytvoření funkce double
na jeho základně:
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
Volání na mul.bind(null, 2)
vytvoří novou funkci double
který předává volání na mul
, oprava null
jako kontext a 2
jako první argument. Další argumenty jsou předány „jak jsou“.
Tomu se říká aplikace částečných funkcí – novou funkci vytvoříme opravou některých parametrů té stávající.
Upozorňujeme, že this
ve skutečnosti nepoužíváme tady. Ale bind
vyžaduje to, takže musíme vložit něco jako null
.
Funkce triple
v níže uvedeném kódu ztrojnásobí hodnotu:
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
Proč obvykle děláme částečnou funkci?
Výhodou je, že můžeme vytvořit nezávislou funkci s čitelným názvem (double
, triple
). Můžeme jej použít a neposkytovat první argument pokaždé, protože je opraven pomocí bind
.
V jiných případech je částečná aplikace užitečná, když máme velmi obecnou funkci a chceme její méně univerzální variantu pro pohodlí.
Máme například funkci send(from, to, text)
. Potom uvnitř user
objekt, můžeme chtít použít jeho částečnou variantu:sendTo(to, text)
který posílá aktuální uživatel.
Přechod částečným bez kontextu
Co když bychom chtěli opravit některé argumenty, ale ne kontext this
? Například pro metodu objektu.
Nativní bind
to nedovoluje. Nemůžeme jen tak vynechat kontext a přejít k argumentům.
Naštěstí funkce partial
pro vazbu lze snadno implementovat pouze argumenty.
Takhle:
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// Usage:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// add a partial method with fixed time
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// Something like:
// [10:00] John: Hello!
Výsledek partial(func[, arg1, arg2...])
volání je obal (*)
který volá func
s:
- Stejné
this
jak to dostane (prouser.sayNow
označte touser
) - Pak mu dá
...argsBound
– argumenty zpartial
volání ("10:00"
) - Pak mu dá
...args
– argumenty zadané obalu ("Hello"
)
Se syntaxí šíření je to tak snadné, že?
K dispozici je také připravená _.částečná implementace z knihovny lodash.
Shrnutí
Metoda func.bind(context, ...args)
vrátí „vázanou variantu“ funkce func
který opravuje kontext this
a první argumenty, pokud jsou uvedeny.
Obvykle používáme bind
opravit this
pro objektovou metodu, abychom ji mohli někam předat. Například na setTimeout
.
Když opravíme některé argumenty existující funkce, výsledná (méně univerzální) funkce se nazývá částečně použitá nebo částečné .
Částečné části jsou vhodné, když nechceme opakovat stále stejný argument. Jako když máme send(from, to)
funkce a from
by měl být vždy stejný pro náš úkol, můžeme získat částečný a pokračovat v něm.