Vazba funkcí

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 (pro user.sayNow označte to user )
  • Pak mu dá ...argsBound – argumenty z partial 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.