Když pÅedáváme objektové metody jako callbacky, napÅÃklad do setTimeout, nastává známý problém: âztráta thisâ.
V této kapitole uvidÃme způsoby, jak jej opravit.
Ztráta âthisâ
PÅÃklady ztráty this jsme už vidÄli. Jakmile je metoda pÅedána nÄkde oddÄlenÄ od objektu, this je ztraceno.
TÃmto způsobem k tomu může dojÃt pÅi použità setTimeout:
let uživatel = {
kÅestnÃJméno: "Jan",
ÅekniAhoj() {
alert(`Ahoj, ${this.kÅestnÃJméno}!`);
}
};
setTimeout(uživatel.ÅekniAhoj, 1000); // Ahoj, undefined!
Jak vidÃme, výstup nezobrazà jako this.kÅestnÃJméno âJanâ, ale undefined!
Je to proto, že setTimeout obdržà funkci uživatel.ÅekniAhoj oddÄlenÄ od objektu. Poslednà Åádek lze pÅepsat na:
let f = uživatel.ÅekniAhoj;
setTimeout(f, 1000); // ztráta kontextu uživatel
Metoda setTimeout v prohlÞeÄi je trochu zvláštnÃ: pÅi volánà funkce nastavà this=window (v Node.js se this nastavà na objekt ÄasovaÄe, ale na tom zde opravdu nezáležÃ). Takže pro this.kÅestnÃJméno se funkce pokusà naÄÃst window.kÅestnÃJméno, které neexistuje. V jiných podobných pÅÃpadech se this obvykle prostÄ nastavà na undefined.
Tato úloha je vcelku typická â chceme pÅedat objektovou metodu nÄkam jinam (zde do plánovaÄe), kde bude volána. Jak zajistit, aby byla volána ve správném kontextu?
ÅeÅ¡enà 1: obal
Nejjednoduššà ÅeÅ¡enà je použÃt obalovou funkci:
let uživatel = {
kÅestnÃJméno: "Jan",
ÅekniAhoj() {
alert(`Ahoj, ${this.kÅestnÃJméno}!`);
}
};
setTimeout(function() {
uživatel.ÅekniAhoj(); // Ahoj, Jan!
}, 1000);
Nynà to funguje, protože funkce si naÄte uživatel z vnÄjÅ¡Ãho lexikálnÃho prostÅedà a pak tuto metodu normálnÄ zavolá.
Totéž, ale kratÅ¡Ã:
setTimeout(() => uživatel.ÅekniAhoj(), 1000); // Ahoj, Jan!
Vypadá to dobÅe, ale v našà struktuÅe kódu se objevuje drobná zranitelnost.
Co když se pÅed spuÅ¡tÄnÃm setTimeout (je tam jednosekundová prodleva!) zmÄnà hodnota promÄnné uživatel? Pak bude náhle zavolán nesprávný objekt!
let uživatel = {
kÅestnÃJméno: "Jan",
ÅekniAhoj() {
alert(`Ahoj, ${this.kÅestnÃJméno}!`);
}
};
setTimeout(() => uživatel.ÅekniAhoj(), 1000);
// ...hodnota promÄnné uživatel se bÄhem 1 sekundy zmÄnÃ
uživatel = {
ÅekniAhoj() { alert("Jiný uživatel v setTimeout!"); }
};
// Jiný uživatel v setTimeout!
NásledujÃcà ÅeÅ¡enà nám zaruÄuje, že se nic takového nestane.
ÅeÅ¡enà 2: metoda bind
Funkce poskytujà vestavÄnou metodu bind, která nám umožÅuje this pevnÄ nastavit.
Základnà syntaxe je:
// složitÄjšà syntaxe pÅijde o nÄco pozdÄji
let vázanáFunkce = funkce.bind(kontext);
Výsledkem funkce.bind(kontext) je speciálnà âexotický objektâ podobný funkci, který lze volat jako funkci a který transparentnÄ pÅedá volánà funkci funkce s nastavenÃm this=kontext.
Jinými slovy, volánà funkce vázanáFunkce je jako volánà funkce s pevným this.
NapÅÃklad zde funkceUživatel pÅedá volánà funkci funkce s this=uživatel:
let uživatel = {
kÅestnÃJméno: "Jan"
};
function funkce() {
alert(this.kÅestnÃJméno);
}
let funkceUživatel = funkce.bind(uživatel);
funkceUživatel(); // Jan
Zde je funkce.bind(uživatel) âvázanou variantouâ funkce s napevno nastaveným this=uživatel.
VÅ¡echny argumenty se pÅedávajà do původnà funkce beze zmÄny, napÅÃklad:
let uživatel = {
kÅestnÃJméno: "Jan"
};
function funkce(vÄta) {
alert(vÄta + ', ' + this.kÅestnÃJméno);
}
// navážeme this na uživatele
let funkceUživatel = funkce.bind(uživatel);
funkceUživatel("Ahoj"); // Ahoj, Jan (argument "Ahoj" je pÅedán a this=uživatel)
Zkusme to nynàs objektovou metodou:
let uživatel = {
kÅestnÃJméno: "Jan",
ÅekniAhoj() {
alert(`Ahoj, ${this.kÅestnÃJméno}!`);
}
};
let ÅekniAhoj = uživatel.ÅekniAhoj.bind(uživatel); // (*)
// můžeme ji spustit bez objektu
ÅekniAhoj(); // Ahoj, Jan!
setTimeout(ÅekniAhoj, 1000); // Ahoj, Jan!
// i když se hodnota promÄnné uživatel bÄhem 1 sekundy zmÄnÃ,
// ÅekniAhoj použije pÅedem navázanou hodnotu, která odkazuje na původnà objekt uživatel
uživatel = {
ÅekniAhoj() { alert("Jiný uživatel v setTimeout!"); }
};
Na Åádku (*) vezmeme metodu uživatel.ÅekniAhoj a navážeme ji na uživatel. Funkce ÅekniAhoj je âvázanáâ funkce, která může být volána samostatnÄ nebo pÅedána do setTimeout â na tom nezáležÃ, kontext bude správnÄ.
Zde vidÃme, že argumenty se pÅedajà beze zmÄny, jen this bude pevnÄ nastaveno funkcà bind:
let uživatel = {
kÅestnÃJméno: "Jan",
Åekni(vÄta) {
alert(`${vÄta}, ${this.kÅestnÃJméno}!`);
}
};
let Åekni = uživatel.Åekni.bind(uživatel);
Åekni("Ahoj"); // Ahoj, Jan! (argument "Ahoj" je pÅedán do funkce Åekni)
Åekni("Nashle"); // Nashle, Jan! (argument "Nashle" je pÅedán do funkce Åekni)
bindAllJestliže objekt obsahuje mnoho metod a my je plánujeme aktivnÄ pÅedávat, můžeme je navázat vÅ¡echny v cyklu:
for (let klÃÄ in uživatel) {
if (typeof uživatel[klÃÄ] == 'function') {
uživatel[klÃÄ] = uživatel[klÃÄ].bind(uživatel);
}
}
Knihovny JavaScriptu poskytujà funkce i pro praktické hromadné navazovánÃ, napÅ. _.bindAll(objekt, názvyMetod) v knihovnÄ Lodash.
ÄásteÄné funkce
Doposud jsme hovoÅili pouze o vázánà this. UÄiÅme dalšà krok.
Můžeme vázat nejen this, ale i argumenty. To se dÄlá zÅÃdkakdy, ale obÄas se to může hodit.
Ãplná syntaxe funkce bind:
let vázanáFunkce = funkce.bind(kontext, [arg1], [arg2], ...);
Umožnà nám navázat kontext jako this a poÄáteÄnà argumenty funkce.
MÄjme napÅÃklad funkci násobenà krát(a, b):
function krát(a, b) {
return a * b;
}
PoužitÃm bind vytvoÅme funkci dvakrát, která na nà bude založena:
function krát(a, b) {
return a * b;
}
let dvakrát = krát.bind(null, 2);
alert( dvakrát(3) ); // = krát(2, 3) = 6
alert( dvakrát(4) ); // = krát(2, 4) = 8
alert( dvakrát(5) ); // = krát(2, 5) = 10
Volánà krát.bind(null, 2) vytvoÅà novou funkci dvakrát, která pÅedává volánà funkci krát, pÅiÄemž napevno nastavà null jako kontext a 2 jako prvnà argument. Dalšà argumenty se pÅedajà beze zmÄny.
To se nazývá ÄásteÄná (parciálnÃ) aplikace funkce â vytvoÅÃme novou funkci tak, že napevno nastavÃme nÄkteré parametry existujÃcà funkce.
ProsÃme vÅ¡imnÄte si, že zde ve skuteÄnosti nepoužÃváme this. Funkce bind jej vÅ¡ak vyžaduje, takže musÃme nÄco pÅedat, tÅeba null.
V následujÃcÃm kódu funkce tÅikrát hodnotu ztrojnásobÃ:
function krát(a, b) {
return a * b;
}
let tÅikrát = krát.bind(null, 3);
alert( tÅikrát(3) ); // = krát(3, 3) = 9
alert( tÅikrát(4) ); // = krát(3, 4) = 12
alert( tÅikrát(5) ); // = krát(3, 5) = 15
ProÄ obvykle vytváÅÃme ÄásteÄnou funkci?
Výhodou je, že můžeme vytvoÅit nezávislou funkci s Äitelným názvem (dvakrát, tÅikrát). Můžeme ji použÃvat, aniž bychom pokaždé museli uvádÄt prvnà argument, jelikož ten je pevnÄ nastaven pomocà bind.
V jiných pÅÃpadech je ÄásteÄná aplikace užiteÄná, když máme velmi generickou funkci a pro své pohodlà chceme jejà ménÄ univerzálnà variantu.
NapÅÃklad máme funkci poÅ¡li(odKoho, komu, text). UvnitÅ objektu uživatel pak můžeme chtÃt použÃvat jejà ÄásteÄnou variantu: poÅ¡liNÄkomu(komu, text), která bude posÃlat od aktuálnÃho uživatele.
ÄásteÄná funkce bez kontextu
Co kdybychom chtÄli napevno nastavit nÄkteré argumenty, ale ne kontext this? NapÅÃklad v objektové metodÄ.
Nativnà bind nám to neumožÅuje. Nemůžeme jednoduÅ¡e vypustit kontext a pÅeskoÄit na argumenty.
NaÅ¡tÄstà můžeme snadno implementovat funkci ÄásteÄná, která bude vázat pouze argumenty.
NapÅÃklad takto:
function ÄásteÄná(funkce, ...vázanéArgumenty) {
return function(...argumenty) { // (*)
return funkce.call(this, ...vázanéArgumenty, ...argumenty);
}
}
// PoužitÃ:
let uživatel = {
kÅestnÃJméno: "Jan",
Åekni(Äas, vÄta) {
alert(`[${Äas}] ${this.kÅestnÃJméno}: ${vÄta}!`);
}
};
// pÅidáme ÄásteÄnou metodu s pevným Äasem
uživatel.ÅekniNynà = ÄásteÄná(uživatel.Åekni, new Date().getHours() + ':' + new Date().getMinutes());
uživatel.ÅekniNynÃ("Ahoj");
// NÄco jako:
// [10:00] Jan: Ahoj!
Výsledkem volánà ÄásteÄná(funkce[, arg1, arg2...]) je obal (*), který volá funkci funkce:
- Se stejným
this, které obdržà (pro volánÃuživatel.ÅekniNynÃto jeuživatel). - Pak jà pÅedá
...vázanéArgumentyâ argumenty z volánà funkceÄásteÄná("10:00"). - Pak jà pÅedá
...argumentyâ argumenty pÅedané obalu ("Ahoj").
S roztaženou syntaxà je tak lehké to udÄlat, že?
NavÃc v knihovnÄ Lodash existuje již pÅipravená implementace _.partial.
ShrnutÃ
Metoda funkce.bind(kontext, ...argumenty) vracà âvázanou variantuâ funkce funkce, která pevnÄ nastavà kontext this a prvnà argumenty, jsou-li pÅedány.
Obvykle použÃváme bind k pevnému nastavenà this pro objektovou metodu, abychom ji mohli nÄkam pÅedat, napÅÃklad do funkce setTimeout.
Když pevnÄ nastavÃme nÄkteré argumenty existujÃcà funkce, výsledná (ménÄ univerzálnÃ) funkce se nazývá ÄásteÄnÄ (parciálnÄ) aplikovaná nebo ÄásteÄná (parciálnÃ).
ÄásteÄná funkce se hodÃ, když nechceme stále znovu a znovu opakovat tentýž argument. NapÅÃklad máme-li funkci poÅ¡li(odKoho, komu) a argument odKoho by mÄl být v našà úloze vždy stejný, můžeme vytvoÅit ÄásteÄnou funkci a pracovat s nÃ.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)