JavaScript Closures – překvapení Java programátora
Javascript používám několik let, snad už od doby kdy jsem na univerzitě začal koketovat s webem. Celou dobu ho používám jen na jednoduché skriptování bez ambic na jakýkoliv propracovanější programovací model. S nástupem kvalitních frameworků jako je třeba jQuery, PrototypeJS, MooTools, Script.aculo.us a další, je člověk přinucen ponořit se do tajů JavaScriptu hlouběji a narazí na věci o kterých se mu před tím ani nesnilo. V tomto článku bych se s vámi rád podělil o pár zkušeností a především odkazů na kvalitní články o tzv. Closures v JavaScriptu. Dopředu upozorňuji, že nejsem žádný JavaScript guru a že čerpám především z odkazovaných článků a z několika projektů, kde jsem díky jQuery a DWR s closures přišel do styku.
Closures jsou i pro lamky
Kapitolku začnu stejným názvem jako článek, který mě do problematiky closures zasvětil asi nejvíc. Co je tedy vlastně ta “Closure”?
Closure je v základu ukazatel na funkci, který je možné přiřadit jako hodnotu proměnné, vrátit ji jako návratovou hodnotu jiné funkce nebo použít jako parametr volání funkce. Následující příklad obsahuje validní JavaScript kód:
Pokud se vám zdá zápis s použitím function() {} – tzn. anonymní funkce nepřehledný a složitý je možné i toto použití (nicméně pozor tímto nevytváříme closures v pravém slova smyslu – v následujícím příkladě pracujeme pouze s ukazateli na metodu, jak se dozvíte o pár odstavců níže):
Prozatím jsme si na příkladech ukázali pouze první vlastnost closures a to je možnost používat “ukazatel” na metodu jako proměnnou (zjednodušeně řečeno). Druhá důležitá vlastnost closures je však zachování lokálního stacku proměnných metody, kde je closures vytvořena. Na první pohled to zní složitě, nicméně princip je relativně jednoduchý.
Podobně jako v Javě jsou i v JavaScriptu lokální proměnné metody uvolňovány po skončení této metody (respektive uvolněny v některém z následujících cyklů GC) – samozřejmě jen tehdy, pokud je nespojíme s objektem s delší životností (tzn. globální proměnné, DOM stromu prohlížeče apod.). Pokud v naší metodě vytvoříme closure a ta se dostane mimo vlastní metodu (třeba je použita jako návratová hodnota), není stack lokálních proměnných metody uvolněn, ale zůstává v paměti pro použití z dané closure (closure má delší životnost jak metoda ve které byla vytvořena – žije tak dlouhod dokud je odkaz na closure uchován v nějaké žijící proměnné). Chování by odpovídalo situaci, jako kdyby closure v sobě obsahovala reference na všechny lokální proměnné metody, ve které byla vytvořena.
Celý princip si můžeme ukázat na následujícím příkladě
Closure si nedrží referenci pouze na proměnné metody, která closure vytvořila, ale na celý strom volání metod až k metodě, která closure vytvořila. Opět lze předvést na následujícím příkladě.
Pasti, pasti, pastičky
Closures jsou velmi silným nástrojem JavaScriptu, ale už na předchozích příkladech jste asi postřehli, bez znalosti principů v pozadí, to může být poměrně velká magie. A to jsme teprve na začátku. V dalších odstavcích chci probrat několik pastiček, na které můžeme narazit a na kterých si velmi jednoduše můžeme vylámat zuby (mě za tento týden zbyly už jenom dvě stoličky
).
Všechny closures vytvořené ve stejné metodě sdílejí stack
Jak již bylo výše řečeno – closures si nedrží kopie proměnných metody, která je vytvořila ale referenci na stack. To znamená, že pokud v metodě vytvoříme více closures budou všechny přistupovat ke stejným proměnným. To si lze deklarovat na dalším příkladě:
Jak je vidno z kódu, změna hodnoty proměnné způsobená jednou closure vytvořenou ve stejném volání metody se promítá při práci se stejnou proměnnou v jiné closure vytvořené ve stejném volání metody. Na první pohled možná těžko srozumitelná věta, ale kdy ji spojíte s průzkumem kódu bude vám brzy jasno.
Druhou důležitou věcí je to, že druhé volání metody getFunctionSet vytváří samostatný stack pro lokální proměnné, takže druhá vytvořená sada closures pracuje s odlišnou proměnnou number než první sada closures. Proměnná je sdílena pouze v rámci jednoho lokálního kontextu (v našem případě jedné sady closures).
Closure přistupuje vždy k aktuální hodnotě proměnné ve chvíli volání
Z minulého příkladu by tento fakt mohl být patrný, ale pro jistotu ho ještě zdůrazním. Tím že si closure drží referenci a nikoliv kopii proměnných, přistupuje ve chvíli svojí exekuce k aktuálním hodnotám daných proměnných. Velmi jednoduchý příklad demonstruje tuto záludnost:
Z této pasti se dostaneme s pomocí deklarace objektu, v jehož konstruktoru vytvoříme vnitřní proměnnou objektu, do které uložíme hodnotu lokální proměnné v době, kdy se vytváří instance objektu. Pomocí metod tohoto objektu pak můžeme přistupovat k vnitřním proměnným, které jsou již kopií a změny hodnot původních lokálních proměnných metody, kde byl objekt vytvořen již tyto proměnné neovlivní.
Tohle byla pro mě už tak trochu vyšší dívčí do doby než jsem si příklad napsal.
Loop proměnné vám zamotají pěkně hlavu
Tohle je přesně ta pastička, kvůli které jsem začal princip fungování closures zkoumat. Chování opět vychází ze stále opakované věty, že closure přistupuje vždy k aktuální hodnotě proměnné ve chvíli volání. Nejlépe si problémek rozebrat na příkladě:
Pokud neznáte pozadí fungování closures, zcela jistě začnete s naivní implementací jak je uvedena v příkladě (respektive já takhle začal) a pak jen kroutíte hlavou. Vysvětlení je prosté – v closure přistupujete k proměnným ve stavu v jakém jsou v momentu, kdy se closure vykonává. Ve chvíli, kdy se spouští ta v našem příkladě, je již loop ukončen a proměnná i=3 (oproti Javě, kde by proměnná mimo scope loopu neexistovala). Na čtvrté pozici pole však již žádná hodnota není a proto se dostaneme jen k “undefined”.
V tomto případě musíme zajistit zafixování hodnoty proměnné ve chvíli, kdy jsme uvnitř toho loopu a máme k dispozici očekávanou hodnotu iterační proměnné. To vyžaduje vykopírování hodnoty někam mimo lokální stack metody, která vytváří closure. Možná jsem to nepopisuji úplně přesně, ale doufám, že moje kostrbaté vysvětlení bude v kombinaci s příkladem pro výsledné pochopení stačit. Vykopírování aktuální hodnoty můžeme docílit minimálně dvěma způsoby. Vytvořením DTO v jehož konstruktoru zafixujeme hodnotu aktuální v daný moment (toto řešení jsme již použili v minulém příkladě), nebo vytvořením funkce “přes koleno”, která také obnáší vytvoření kopie hodnoty.
Závěr
Zažití použití closures v JS vyžaduje nějaký čas a experimentování. Mně osobně k tomu donutilo používání Ajaxu (konkrétně DWR spolu s Prototype.js nebo jQuery) a vůbec toho nelituji. Přestože Closures, tak jak je o nich diskutováno v Javě by byly ve výsledku v řadě ohledů odlišné, základní princip bude plus mínus zachován (pokud closures v Javě vůbec kdy budou) a já jsem minimálně nakouknul pod pokličku toho mechanismu někde, kde již řadu let funguje. Rozhodně to nebyl marný výlet a já jsem rád, že jsem se mohl zase něco dalšího naučit.
V příštím článku bych se s vámi chtěl podělit o nějaké zkušenosti s efekty v jQuery, které byly prapůvodní příčinou mého zájmu o JavaScript a motorem pro napsání tohoto článku …
Reference
Pokud vám budou některé příklady nejasné, nebo moje vysvětlení nepřesné, určitě koukněte na následující odkazy, z nichž jsem informace čerpal:
- JavaScript Closures for Dummies – výborný blogpost, který byl pro mě tím světlem na konci tunelu
- Inroduction to JavaScript functions – úvod do práce s funkcemi v JS
- Private Members in JavaScript – skvělý článek popisující rozdíly mezi veřejnými a priváními metodamy / atributy javascriptového objektu
- JavaScript closures in for-loops – rozbor pasti s loop proměnnými
- Funkce v JavaScriptu – článek v češtině obecně o funkcích v JS
Příklady zobrazené v tomto článku je možné si stáhnout zde:




Tak o tehle vlastnostech JavaScriptu jsem taky nevedel. Diky za prehledne shrnuti.
Parada. Proste parada. Super clanek. Hned jsu poslat link kolegum.
Na closures v JS je podle mne největší pastička v tom, že mohou být volány i jako funkce i jako metody, takže dopředu nikdy není jasné, na co bude odkazovat uvnitř closure “this”.
No a ta hlavní výhoda closures – že si s sebou vezou lokální kontext – mi osobně připadá jenom jako trošku zakamuflované globální proměnné, aby se programátorům, kteří někdy slyšeli, že globální proměnné jsou fuj, neježily hrůzou vlasy na hlavě. Jinak je to v podstatě to samé, umožňuje to pracovat s nějakou proměnnou mimo její kontext, aniž bych tu proměnnou musel explicitně předávat jako parametr.
Chápu, že programátoři v Javě apod. jsou zvyklí operovat se zásobníkem jako místem, kam se ukládají lokální proměnné, ale vzhledem k tomu, že tady “lokální” proměnné mohou svůj lexikální obor platnosti “přežít”, ve skutečnosti nejsou na zásobníku, ale normálně na heapu. Trochu jsem o tom psal, ve spojitosti s Javou, tady: http://www.abclinuxu.cz/blog/variace/2006/10/prvotridni-java
> ta hlavní výhoda closures – že si s sebou vezou lokální kontext – mi osobně připadá jenom jako trošku zakamuflované globální proměnné
Jo jo jo jo jo, to jsou úúúúúplně normální globální proměnné, akorát že nejsou globální
“Doba života” se sice liší od lexikálního oboru platnosti, ale ten je pořád zachován, žádná “práce s proměnnou mimo její kontext” neexistuje.
Díky za komentáře. Z nich si pro sebe vyvozuji to, že přestože jsem si už tak trochu zažil chování closures, ještě stále dost plavu na povrchu v detailech konkrétní implementace v JavaScript enginu. No snad nikoho tímhle článkem nepřivedi příliš na scestí.
Super clanek, snad prvni vec o javascriptu, co jsem docetl az do konce. Skoda, ze porad nezmenil muj nazor na javascript
Díky za pěkný článek!
Pekne zhrnutie,
rovnako som v nedavnej dobe pracoval s DWR a JQuery. Javascriptovy kod sa nam zacal hromadit skoro na kazdej stranke a tak bola nutnost napisat nejaku abstrakciu na volanie DWR a veci okolo toho, tam sa closury na par miestach vyuzili.
Vrelo odporucam knihu, Pro Javascript Techniques od autora JQuery. Priznam sa, ze to bola moja prva JS kniha, ktoru som cital (pred tym som cital vacsinou zdroje na webe, ktorych je v celku dost). Je prelozena aj do cestiny ale v anglictine je to viac ono. Hned od uvodu sa venuje objektovemu javascriptu, closuram a podobne, proste veci, ktore su velmi podstatne ale nejak som sa o nich vo webovych tutorialoch nikdy nedocital. Priznam sa, ze predtym som Javascript nemal rad, ale po prestudovani ako veci funguju som prisiel tomuto jazyku na chut.
PS: momentalne sa hram s groovy a s jeho closurami, a po mojom testiku mozem potvrdit, ze fungovanie je velmi podobne javascriptu aspon co sa tyka tych kontextov:
def getFunction1(param1) {
return [
{it ->
param1++;
println "$it $param1"
},
{it ->
param1++;
println "$it $param1"
}
]
}
getFunction1(1).each {it(“param”)}
Vysledok:
>> param 2
>> param 3
// fix
def getFunction2(param1) {
return [
{it ->
def _param1 = {param1}();
_param1++;
println "$it $_param1"
},
{it ->
def _param1 = {param1}();
_param1++;
println "$it $_param1"
}
]
}
getFunction2(1).each {it(“param”)}
Vysledok:
>> param 2
>> param 2
Na Groovy se chystám už od jara. Odkládám to už moc dlouho – o Vánocích se do toho pustím a čím dál víc se na to těším. Díky za info.
Tak tento clanok je fakt super. Makam momentalne na vacsiom projekte (projektoch), kde vyuzivame aj dost Javascriptu a komunikaciu s Ajaxom a toto vyzera velmi dobre. Asi to aj pouzijem, len to musim este poriadne kuknut.