JavaScript poskytuje výjimečnou flexibilitu při práci s funkcemi. Lze je předávat, používat jako objekty a nyní se podíváme, jak je předat hovory mezi nimi a zdobit je.
Transparentní ukládání do mezipaměti
Řekněme, že máme funkci slow(x)
který je náročný na CPU, ale jeho výsledky jsou stabilní. Jinými slovy, pro stejné x
vždy vrací stejný výsledek.
Pokud je funkce volána často, můžeme chtít uložit (zapamatovat si) výsledky do mezipaměti, abychom nemuseli trávit čas navíc přepočítáváním.
Ale místo přidání této funkce do slow()
vytvoříme funkci wrapper, která přidá ukládání do mezipaměti. Jak uvidíme, má to mnoho výhod.
Zde je kód a vysvětlení následují:
function slow(x) {
// there can be a heavy CPU-intensive job here
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // if there's such key in cache
return cache.get(x); // read the result from it
}
let result = func(x); // otherwise call func
cache.set(x, result); // and cache (remember) the result
return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // slow(1) is cached and the result returned
alert( "Again: " + slow(1) ); // slow(1) result returned from cache
alert( slow(2) ); // slow(2) is cached and the result returned
alert( "Again: " + slow(2) ); // slow(2) result returned from cache
V kódu výše cachingDecorator
je dekoratér :speciální funkce, která přebírá jinou funkci a mění její chování.
Myšlenka je taková, že můžeme zavolat cachingDecorator
pro jakoukoli funkci a vrátí obálku mezipaměti. To je skvělé, protože můžeme mít mnoho funkcí, které by takovou funkci mohly využívat, a vše, co musíme udělat, je použít cachingDecorator
jim.
Oddělením ukládání do mezipaměti od kódu hlavní funkce také zjednodušujeme hlavní kód.
Výsledek cachingDecorator(func)
je „obal“:function(x)
který „zabalí“ volání func(x)
do logiky ukládání do mezipaměti:
Z vnějšího kódu, zabalený slow
funkce dělá stále to samé. Do jeho chování byl přidán aspekt ukládání do mezipaměti.
Abychom to shrnuli, existuje několik výhod použití samostatného cachingDecorator
místo změny kódu slow
sám:
cachingDecorator
je opakovaně použitelný. Můžeme to aplikovat na jinou funkci.- Logika ukládání do mezipaměti je samostatná, nezvýšila složitost
slow
sám (pokud nějaký byl). - V případě potřeby můžeme zkombinovat více dekoratérů (další dekoraté budou následovat).
Použití „func.call“ pro kontext
Výše zmíněný dekorátor ukládání do mezipaměti není vhodný pro práci s objektovými metodami.
Například v kódu níže worker.slow()
přestane fungovat po dekoraci:
// we'll make worker.slow caching
let worker = {
someMethod() {
return 1;
},
slow(x) {
// scary CPU-heavy task here
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
// same code as before
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func(x); // (**)
cache.set(x, result);
return result;
};
}
alert( worker.slow(1) ); // the original method works
worker.slow = cachingDecorator(worker.slow); // now make it caching
alert( worker.slow(2) ); // Whoops! Error: Cannot read property 'someMethod' of undefined
Chyba se vyskytuje v řádku (*)
který se pokouší o přístup k this.someMethod
a selže. Vidíte proč?
Důvodem je, že obal volá původní funkci jako func(x)
v řádku (**)
. A když je takto volána, funkce dostane this = undefined
.
Pokud bychom se pokusili spustit, pozorovali bychom podobný příznak:
let func = worker.slow;
func(2);
Takže obal předá volání původní metodě, ale bez kontextu this
. Proto ta chyba.
Pojďme to opravit.
Existuje speciální vestavěná metoda funkce func.call(context, …args), která umožňuje volat funkci s explicitním nastavením this
.
Syntaxe je:
func.call(context, arg1, arg2, ...)
Běží func
poskytuje první argument jako this
a další jako argumenty.
Jednoduše řečeno, tato dvě volání dělají téměř totéž:
func(1, 2, 3);
func.call(obj, 1, 2, 3)
Oba volají func
s argumenty 1
, 2
a 3
. Jediný rozdíl je v tom, že func.call
také nastaví this
na obj
.
Jako příklad v níže uvedeném kódu nazýváme sayHi
v kontextu různých objektů:sayHi.call(user)
běží sayHi
poskytuje this=user
a další řádek nastaví this=admin
:
function sayHi() {
alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// use call to pass different objects as "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin
A zde používáme call
zavolejte na say
s daným kontextem a frází:
function say(phrase) {
alert(this.name + ': ' + phrase);
}
let user = { name: "John" };
// user becomes this, and "Hello" becomes the first argument
say.call( user, "Hello" ); // John: Hello
V našem případě můžeme použít call
v obalu předat kontext původní funkci:
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // "this" is passed correctly now
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // now make it caching
alert( worker.slow(2) ); // works
alert( worker.slow(2) ); // works, doesn't call the original (cached)
Nyní je vše v pořádku.
Aby bylo vše jasné, podívejme se hlouběji na this
je předáno:
- Po dekoraci
worker.slow
je nyní obalfunction (x) { ... }
. - Když tedy
worker.slow(2)
je proveden, obal dostane2
jako argument athis=worker
(je to objekt před tečkou). - Uvnitř obálky, za předpokladu, že výsledek ještě není uložen do mezipaměti,
func.call(this, x)
předává aktuálníthis
(=worker
) a aktuální argument (=2
) na původní metodu.
Přejít na více argumentů
Nyní uděláme cachingDecorator
ještě univerzálnější. Doposud to fungovalo pouze s funkcemi s jedním argumentem.
Nyní, jak uložit do mezipaměti multi-argument worker.slow
metoda?
let worker = {
slow(min, max) {
return min + max; // scary CPU-hogger is assumed
}
};
// should remember same-argument calls
worker.slow = cachingDecorator(worker.slow);
Dříve pro jeden argument x
mohli bychom jen cache.set(x, result)
pro uložení výsledku a cache.get(x)
získat to. Nyní si ale musíme zapamatovat výsledek pro kombinaci argumentů (min,max)
. Nativní Map
bere jako klíč pouze jednu hodnotu.
Existuje mnoho možných řešení:
- Implementujte novou (nebo použijte datovou strukturu podobnou mapě třetí strany), která je všestrannější a umožňuje použití více klíčů.
- Používejte vnořené mapy:
cache.set(min)
budeMap
který ukládá pár(max, result)
. Můžeme tedy získatresult
jakocache.get(min).get(max)
. - Spojte dvě hodnoty do jedné. V našem konkrétním případě stačí použít řetězec
"min,max"
jakoMap
klíč. Pro flexibilitu můžeme povolit poskytnutí hašovací funkce pro dekoratéra, který ví, jak vytvořit jednu hodnotu z mnoha.
Pro mnoho praktických aplikací je 3. varianta dostačující, takže se jí budeme držet.
Také musíme předat nejen x
, ale všechny argumenty v func.call
. Připomeňme, že v function()
můžeme získat pseudopole jeho argumentů jako arguments
, takže func.call(this, x)
by měl být nahrazen func.call(this, ...arguments)
.
Zde je výkonnější cachingDecorator
:
let worker = {
slow(min, max) {
alert(`Called with ${min},${max}`);
return min + max;
}
};
function cachingDecorator(func, hash) {
let cache = new Map();
return function() {
let key = hash(arguments); // (*)
if (cache.has(key)) {
return cache.get(key);
}
let result = func.call(this, ...arguments); // (**)
cache.set(key, result);
return result;
};
}
function hash(args) {
return args[0] + ',' + args[1];
}
worker.slow = cachingDecorator(worker.slow, hash);
alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)
Nyní pracuje s libovolným počtem argumentů (ačkoli hashovací funkce by také musela být upravena tak, aby umožňovala libovolný počet argumentů. Zajímavý způsob, jak to zvládnout, bude popsán níže).
Existují dvě změny:
- Na řádku
(*)
voláhash
vytvořit jeden klíč zarguments
. Zde používáme jednoduchou funkci „spojení“, která převede argumenty na(3, 5)
do klíče"3,5"
. Složitější případy mohou vyžadovat jiné hašovací funkce. - Poté
(**)
používáfunc.call(this, ...arguments)
předat jak kontext, tak všechny argumenty, které obal dostal (nejen ten první), původní funkci.
func.apply
Místo func.call(this, ...arguments)
mohli bychom použít func.apply(this, arguments)
.
Syntaxe vestavěné metody func.apply je:
func.apply(context, args)
Spouští func
nastavení this=context
a pomocí objektu podobného poli args
jako seznam argumentů.
Jediný rozdíl v syntaxi mezi call
a apply
je to call
očekává seznam argumentů, zatímco apply
vezme s sebou objekt podobný poli.
Takže tato dvě volání jsou téměř ekvivalentní:
func.call(context, ...args);
func.apply(context, args);
Provádějí stejné volání func
s daným kontextem a argumenty.
Pokud jde o args
, existuje pouze nepatrný rozdíl :
- Syntaxe šíření
...
umožňuje předat iterovatelnéargs
jako seznam nacall
. apply
přijímá pouze poleargs
.
…A pro objekty, které jsou iterovatelné a podobné poli, jako je skutečné pole, můžeme použít kterýkoli z nich, ale apply
bude pravděpodobně rychlejší, protože většina JavaScriptových motorů jej interně lépe optimalizuje.
Předání všech argumentů spolu s kontextem jiné funkci se nazývá přesměrování hovorů .
To je ta nejjednodušší forma:
let wrapper = function() {
return func.apply(this, arguments);
};
Když externí kód zavolá takový wrapper
, je k nerozeznání od volání původní funkce func
.
Vypůjčení metody
Nyní udělejme ještě jedno drobné vylepšení hashovací funkce:
function hash(args) {
return args[0] + ',' + args[1];
}
Zatím funguje pouze na dvou argumentech. Bylo by lepší, kdyby to dokázalo slepit libovolný počet args
.
Přirozeným řešením by bylo použít metodu arr.join:
function hash(args) {
return args.join();
}
...to bohužel nepůjde. Protože voláme hash(arguments)
a arguments
objekt je iterovatelný a podobný poli, ale není skutečným polem.
Volání join
na tom by selhal, jak můžeme vidět níže:
function hash() {
alert( arguments.join() ); // Error: arguments.join is not a function
}
hash(1, 2);
Přesto existuje snadný způsob, jak použít připojení k poli:
function hash() {
alert( [].join.call(arguments) ); // 1,2
}
hash(1, 2);
Tento trik se nazývá půjčování metody .
Vezmeme (vypůjčíme) metodu spojení z běžného pole ([].join
) a použijte [].join.call
spustit v kontextu arguments
.
Proč to funguje?
Je to proto, že interní algoritmus nativní metody arr.join(glue)
je velmi jednoduché.
Převzato ze specifikace téměř „tak jak je“:
- Nechte
glue
být prvním argumentem nebo, pokud žádné argumenty nejsou, pak čárkou","
. - Nechte
result
být prázdný řetězec. - Připojit
this[0]
doresult
. - Připojit
glue
athis[1]
. - Připojit
glue
athis[2]
. - …Udělejte tak do
this.length
položky jsou slepené. - Vraťte
result
.
Technicky to tedy trvá this
a připojí se ke this[0]
, this[1]
…atd společně. Je záměrně napsán způsobem, který umožňuje jakékoli pole this
(není to náhoda, mnoho metod se řídí touto praxí). Proto také funguje s this=arguments
.
Dekorátory a funkční vlastnosti
Obecně je bezpečné nahradit funkci nebo metodu zdobenou, až na jednu maličkost. Pokud původní funkce měla vlastnosti, například func.calledCount
nebo cokoli, pak je ozdobený neposkytne. Protože to je obal. Při jejich používání je tedy třeba být opatrný.
Např. ve výše uvedeném příkladu if slow
funkce měla nějaké vlastnosti, pak cachingDecorator(slow)
je obal bez nich.
Někteří dekoratéři mohou poskytovat své vlastní vlastnosti. Např. dekoratér může spočítat, kolikrát byla funkce vyvolána a jak dlouho to trvalo, a vystavit tyto informace prostřednictvím vlastností obalu.
Existuje způsob, jak vytvořit dekorátory, které udrží přístup k vlastnostem funkcí, ale to vyžaduje použití speciálního Proxy
objekt pro zabalení funkce. Probereme to později v článku Proxy and Reflect.
Shrnutí
Dekoratér je obal kolem funkce, který mění její chování. Hlavní práci stále vykonává funkce.
Dekorátory lze vnímat jako „vlastnosti“ nebo „aspekty“, které lze přidat k funkci. Můžeme přidat jeden nebo přidat mnoho. A to vše beze změny kódu!
Chcete-li implementovat cachingDecorator
, studovali jsme metody:
- func.call(context, arg1, arg2…) – volá
func
s daným kontextem a argumenty. - func.apply(context, args) – volání
func
předánícontext
jakothis
a pole podobnéargs
do seznamu argumentů.
Obecné přesměrování hovorů se obvykle provádí pomocí apply
:
let wrapper = function() {
return original.apply(this, arguments);
};
Viděli jsme také příklad výpůjčky metody když vezmeme metodu z objektu a call
to v kontextu jiného objektu. Je docela běžné vzít metody pole a aplikovat je na arguments
. Alternativou je použít objekt zbývajících parametrů, který je skutečným polem.
Ve volné přírodě je mnoho dekoratérů. Ověřte si, jak dobře jste je zvládli vyřešením úkolů v této kapitole.