Dekoratéři a přeposílání, volejte/přihlaste se

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:

  1. Po dekoraci worker.slow je nyní obal function (x) { ... } .
  2. Když tedy worker.slow(2) je proveden, obal dostane 2 jako argument a this=worker (je to objekt před tečkou).
  3. 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í:

  1. 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íčů.
  2. Používejte vnořené mapy:cache.set(min) bude Map který ukládá pár (max, result) . Můžeme tedy získat result jako cache.get(min).get(max) .
  3. Spojte dvě hodnoty do jedné. V našem konkrétním případě stačí použít řetězec "min,max" jako Map 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íč z arguments . 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 na call .
  • apply přijímá pouze pole args .

…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“:

  1. Nechte glue být prvním argumentem nebo, pokud žádné argumenty nejsou, pak čárkou "," .
  2. Nechte result být prázdný řetězec.
  3. Připojit this[0] do result .
  4. Připojit glue a this[1] .
  5. Připojit glue a this[2] .
  6. …Udělejte tak do this.length položky jsou slepené.
  7. 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 jako this 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.


No