Uzávěry
Uzávěr (angl. closure) je pro mnoho lidí nejsložitějším partem javascriptového programování a patrně sbírkou největších mystérií, která v souvislosti s Javascriptem kolují. Taky jsem kolem toho velmi dlouho tápal a nebyl s to tento problém pevně uchopit za pačesy. Nedávno jsem se tím snad prokousal a k svému překvapení objevil nečekaně snadné a pochopitelné pravidlo a úhel pohledu, z nějž je na uzávěry nejlépe koukat.
Nejprve oč jde. Pokud v Javascriptu vytvoříte globální anonymní funkci, její chování je poměrně jasné – při svém spuštění se chová jako globální a má k dispozici globální proměnné platné v okamžiku svého volání. Problém ale nastává, když takovou anonymní funkci vytvoříme uvnitř jiné funkce. Je vytvořena v určitém lokálním kontextu, ale volána může být kdykoli jindy, většinou v situaci, kdy onen lokální kontext už dávno neexistuje.
function zpracuj(elm) { var x = 1; elm.onclick = function(){ alert(x); } } var elm = document.getElementById('tlacitko1'); zpracuj(elm);
Zavoláme funkci zpracuj
, ta si vytvoří lokální proměnnou x
a následně prvku elm
přiřadí pro zpracování události onclick
novou anonymní funkci, která používá tuto proměnnou x
. Funkce zpracuj
poté skončí a její lokální proměnné (včetně toho x
) zmizí. Ovšem když uživatel klikne na tlačítko, událost onclick
se vyvolá a spustí se ona anonymní funkce, která jí byla přiřazena a která používá proměnnou x
z kontextu, který již neexistuje. Co se má v tuto chvíli stát?
Řešením jsou právě zmíněné uzávěry. Tento princip programovacího jazyka říká, že v těchto případech se má vytvořit jakási záloha kontextu, který byl v době vytvoření oné anonymní funkce platný, a ten bude mít tato funkce k dispozici v době svého volání.
V uvedeném případě tedy událost onclick
dostane přiřazen nejen kód funkce, který se má při jejím vyvolání zpracovat, ale také lokální kontext funkce zpracuj
, který měla v době, kdy tento ovladač vytvářela. V našem případě tedy po kliknutí na tlačítko bude mít ovladač k dispozici kopii proměnné x
a vypíše hodnotu 1.
Ovšem to podstatné, co je jádrem k pochopení celé problematiky uzávěrů je fakt, že tím kontextem se nemyslí stav a hodnoty všech proměnných v okamžiku vytvoření té funkce. Pokud si to člověk promyslí do důsledků, zjistí, že s tím by bylo mrzení až hanba. Zjednodušeně řečeno anonymní funkce v uzávěru dostane požadovaný kontext až ve stavu, v jakém je ve chvíli volání této funkce. A pakliže tento kontext už neexistuje a ta „mateřská“ funkce, která uzávěr vytvořila, již dávno skončila, dostane uzávěr zálohu jejího kontextu v posledním známém stavu. Jinými slovy: uzávěr dostane všechny proměnné, které byly k dispozici při skončení běhu funkce, v níž vznikl.
function zpracuj(elm) { elm.onclick = function(){ alert(x); } var x = 20; x++; } var elm = document.getElementById('tlacitko1'); zpracuj(elm);
Zde opět ve funkci zpracuj
přiřadíme prvku elm
ovladač události onclick
. Ten má zobrazit hodnotu x
. V daném kontextu je to proměnná lokální a z hlediska principu uzávěru vůbec nesejde na tom, jakou měla hodnotu v okamžiku vytvoření toho ovladače, ba ani že v tom okamžiku dokonce ještě nebyla ani deklarována. Ovladač (ona anonymní funkce) pro svůj běh dostane v rámci uzávěru kontext takový, jaký byl až po dokončení funkce zpracuj
– tedy proměnná x
bude deklarována a bude mít hodnotu 21. Což je také hodnota, kterou tento ovladač po kliknutí na tlačítko zobrazí.
A to je v zásadě celý trik s uzávěry. Pokud kdekoli uvnitř nějaké funkce definujeme nějaký ovladač či jinou anonymní funkci, stačí jen myslet na to, že tato funkce nebude mít k dipozici proměnné tak, jak vypadají právě teď, když tu funkci tvoříme, ale tak, jak budou vypadat na konci, až ta vnější funkce skončí – a máme vyhráno. Zkuste si malý test:
<button id="tlacitko1">test 1</button> <button id="tlacitko2">test 2</button> <button id="tlacitko3">test 3</button> <script type="text/javascript"> function zpracuj() { var i, elm; for (i=1;i<=3;i++) { elm = document.getElementById('tlacitko'+i); elm.onclick = function(){ alert(i); } } } zpracuj(); </script>
Co se zobrazí po kliknutí na jednotlivá tlačítka? Kdo na první pohled pozná, že všechna tři zobrazí shodně hodnotu 4, má už uzávěry v malíčku. Kdo hádal něco jiného, nechť si krok po kroku projde činnost funkce zpracuj
, poznamená si někam hodnotu proměnné i
po jejím skončení a pak si zkusí zahrát na ten ovladač, co by asi tak zobrazil.
Nesmí ovšem dojít k mýlce. Uzávěr nedostává nějakou „mrtvou“ kopii kontextu, jde o kontext zcela plnohodnotný. Pokud „mateřská“ funkce dosud běží, pracuje uzávěr přímo v jejím kontextu, pokud už skončila, pracuje v původním kontextu, který zůstal zakonzervován. Pokud v rámci jedné funkce vytvoříme více uzávěrů, nevytvoří se nějaká kopie kontextu pro každý z nich. Budou sdílet týž společný kontext původní funkce a mohou v něm navzájem interagovat.
function zpracuj() { var elm; elm = document.getElementById('tlacitko'); elm.onmouseover = function(){ counter++ } elm.onmouseout = function(){ this.innerHTML = counter } var counter = 100; } zpracuj();
Ve funkci zpracuj
vytváříme dvě anonymní funkce: ovladače pro onmouseover
a onmouseout
. Oba budou při svém spuštění (po vyvolání příslušné události) používat zakonzervovaný kontext funkce zpracuj
, a to pro oba společný. Budou sdílet stejný uzávěr. Již víme, že vůbec nezáleží na tom, že proměnná counter
v době definování ovladačů ještě neexistovala – hlavní je, že existuje v době jejich volání. Obě funkce dostanou sdílený kontext, v němž je proměnná counter
deklarována a má hodnotu 100. Pokud našem tlačítku vyvoláme událost onmouseover
, zvýší se hodnota counter
o jedničku, po vyvolání události onmouseout
se tato hodnota zapíše jako obsah tlačítka; přičemž kontext obou uzávěrů bude trvat dál. Když tedy budeme myší přejíždět nad tlačítkem, bude se v něm postupně zobrazovat hodnoty 101, 102, 103 atd., vždy o jedna vyšší při každém přejezdu.
Tohle & Tamto
Posledním kamenem úrazu bývá použití klíčového slova this
, a to nejčastěji právě v uzávěrech. Může to sice někdy být složité a náročné na přemýšlení, ale stačí jen myslet na to, co toto magické slovo vlastně vyjadřuje. A neříká nic jiného, než že odpovídá na otázku, kdo, resp. kde právě jsem?
Uvnitř definice tříd a objektů odkazuje na objekt samotný a není s tím obvykle žádné trápení. Ovšem narazíme v situaci, kdy zde vytváříme nějaký nový (anonymní) objekt nebo novou (anonymní) funkci a dojde na uplatnění uzávěru. Neboť význam klíčového slova this
se zkoumá a převádí na jemu odpovídající objekt až v okamžiku volání dotyčné funkce – a snadno se stane, že v tu chvíli je odpovědí na onu otázku „kdo jsem / kde jsem“ něco úplně jiného než v době vytváření.
var XYZ = {}; XYZ.test = function(param) { this.X = param; var init = function() { if (this.X==undefined) alert('chyba'); } init(); }; XYZ.test(1);
V našem objektu XYZ z nějakého (jistě dobrého) důvodu přiřazujeme init
anonymní funkci, která zde má otestovat hodnotu, kterou jsme si přiřadili do vlastnosti X
našeho objektu. Ovšem když to v této podobě vyzkoušíme, zjistíme, že to nebude fungovat a chyba se vypíše s jakýmkoli parametrem. Je to proto, že this
vyjadřuje aktuální informaci „kdo jsem / kde jsem“, což v případě běhu oné anonymní funkce už neznamená objekt XYZ
. V okamžiku svého volání se už nenachází v jeho kontextu, je „vytržena“ do kontextu globálního a this
v jejím případě označuje globální objekt. Pokud bychom si místo alert(„chyba“) nechali vypsat hodnotu this
, zjistíme, že jí je globální objekt window
.
Řešení je v těchto případech prosté. Stačí si hodnotu this
poznamenat do nějaké lokální proměnné (obvyklé je that
), která se měnit nebude a všechny anonymní funkce i cizí objekty ji budou mít k dipozici, a to i uvnitř případného uzávěru.
var XYZ = {}; XYZ.test = function(param) { this.X = param; var that = this; var init = function() { if (that.X==undefined) alert('chyba'); } init(); }; XYZ.test(1);
A ejhle, již vše funguje, jak má. Anonymní funkce dostane v rámci uzávěru proměnné z kontextu funkce XYZ.text
, tedy i hodnotu that
odpovídající objektu XYZ
a skutečnost, že hodnota this
se změnila na referenci na úplně jiný objekt, už nás vůbec nemusí trápit.
Stejné je to v případě zpracování ovladačů událostí nebo odkazování funkcí v setTimeout
.
var XYZ = {}; XYZ.test = function(ID) { var that = this; this.elm = document.getElementById(ID); this.elm.onchange = function() { that.aktualizuj(this.value); that.pockej(3000); } this.aktualizuj = function(hodnota) { /* ... */ } this.pockej = function(ms){ setTimeout(that.dokonci,ms); } this.dokonci = function() { alert('hotovo') } }; XYZ.test('selectbox');
Metodě XYZ.test
předáme ID
nějakého HTML selectu. Ta jeho události onchange
přiřadí jako ovladač anonymní funkci, která se přes připravenou lokální proměnnou that
může odkazovat na další metody objektu XYZ
. Při volání toho ovladače odpovídá hodnota this
prvku, na kterém událost vznikla, čehož využije pro zjištění jeho hodnoty, kterou zpracuje a zavolá metodu pockej
. Ta opět díky proměnné that
, kterou mají anonymní funkce v uzávěru dostupnou,zavolá se zpožděním metodu dokonci
. V praxi to bude vypadat tak, že změní-li se hodnota v našem prvku selectbox
, zavolá se metoda aktualizuj
a za 3 sekundy se zavolá metoda dokonci
, která zobrazí hlášku „hotovo“.
Závěr
Vida, ona to nakonec až taková věda není. Proměnné i funkce platí v té části kódu, v níž byly vytvořeny – pokud to bylo na nejvyšší úrovni, jsou dostupné globálně; pokud to bylo uvnitř funkce, jsou dostupné jen uvnitř ní a zvenčí nikoli; vlastnosti a metody objektů jsou veřejně dostupné, jejich lokální proměnné a funkce nikoli. A pakliže uvnitř nějaké funkce vytvoříme anonymní funkci, bude při svém volání pracovat v uzávěru, který zůstal zachován z běhu funkce, která jej vytvořila. Jestli jste v některých otázkách platnosti proměnných v Javascriptu neměli úplně jasno, snad nyní tápete o něco méně. Mně osobně tohle shrnutí docela pomohlo.
Pozn.: Na tento článek jsem se chystal už dva týdny, shodou okolností právě v den, kdy jsem se do něj pustil, vyšlo podobné téma na Smashing Magazine. Nemůžu předstírat, že jsem tento článkem neviděl, nicméně jsem jej nikterak vědomě nekopíroval. Náhody si nevybírají.
Přehled komentářů