Internet Info, s.r.o. Lupa Měšec Podnikatel Root Zdroják DigiZone Slunečnice Vitalia TopDrive KupDnes Navrcholu NovýTarif Dobrý web Weblogy Woko Jagg Computer.cz SK: MojeLinky

Hlavní navigace

Javascript a oblast působnosti proměnných - díl třetí

Poslední díl článku Petra Staníčka o oblasti působnosti proměnných se věnuje uzávěrům (closures) v JavaScriptu a použití klíčového slova this.

Tweetni to Twitter Jaggni to! Jagg Del.icio.us Delicious

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í.

Petr Staníček

Autor je návrhář UI/UX, analytik, grafik, javascriptový vývojář a advocatus diaboli ex offo.

Školení Google+ pro firmy

DW - Školení PPC
  • Jak využít Google+ pro firemní komunikaci a marketing.
  • Čím se liší Google+ od Twitteru a Facebooku z pohledu firemního využití.
  • Jak využít Google+ v souladu s pravidly užívání.
  • Založení Google+ Page (Stránky) krok po kroku, včetně praktických tipů.

Detailní informace o školení Google+ »

Přehled názorů

Re: Javascript a oblast působnosti proměnných - díl třetí
Aichi 17. 8. 2009 10:18
Nový
├ 
Re: Javascript a oblast působnosti proměnných - díl třetí
fos4 17. 8. 2009 10:24
Nový
└ 
Re: Javascript a oblast působnosti proměnných - díl třetí
_ 23. 8. 2009 19:14
Nový
Jestli tomu dobře rozumím
pracj3am 17. 8. 2009 12:22
Nový
└ 
Re: Jestli tomu dobře rozumím
Aichi 17. 8. 2009 13:43
Nový
 
└ 
Re: Jestli tomu dobře rozumím
pracj3am 17. 8. 2009 15:33
Nový
 
 
└ 
Re: Jestli tomu dobře rozumím
Aichi 17. 8. 2009 16:04
Nový
 
 
 
└ 
Re: Jestli tomu dobře rozumím
pracj3am 17. 8. 2009 16:26
Nový
 
 
 
 
├ 
Re: Jestli tomu dobře rozumím
Aichi 17. 8. 2009 17:09
Nový
 
 
 
 
└ 
Re: Jestli tomu dobře rozumím
Adam Hořčica 17. 8. 2009 17:19
Nový
apply / call
cHLeB@ 17. 8. 2009 13:44
Nový
Velikost písmen
Jakub Vrána 17. 8. 2009 16:18
Nový
├ 
Re: Velikost písmen
Vít Šesták 23. 8. 2009 13:52
Nový
│
├ 
Co je to Window
Daniel Steigerwald 23. 8. 2009 20:15
Nový
│
└ 
Re: Velikost písmen
Jakub Vrána 23. 8. 2009 21:32
Nový
└ 
Re: Velikost písmen
Petr Staníček 26. 8. 2009 11:14
Nový
Tisk
radik 18. 8. 2009 11:07
Nový
└ 
Re: Tisk
radik 18. 8. 2009 11:18
Nový
 
└ 
Re: Tisk
Martin Malý 18. 8. 2009 11:21
Nový
Deklarace proměnné po definici uzávěru
Jakub Vrána 20. 8. 2009 11:26
Nový
├ 
Re: Deklarace proměnné po definici uzávěru
_ 22. 8. 2009 21:24
Nový
│
└ 
Re: Deklarace proměnné po definici uzávěru
Vít Šesták 23. 8. 2009 13:55
Nový
│
 
└ 
Re: V. Šesták
_ 23. 8. 2009 17:04
Nový
│
 
 
└ 
Re: V. Šesták
Vít Šesták 23. 8. 2009 17:37
Nový
│
 
 
 
└ 
Re: V. Šesták
_ 23. 8. 2009 17:55
Nový
│
 
 
 
 
└ 
Re: V. Šesták
Vít Šesták 23. 8. 2009 17:58
Nový
└ 
Re: Deklarace proměnné po definici uzávěru
Vít Šesták 23. 8. 2009 19:25
Nový
vznik closure
Michal Augustýn 22. 8. 2009 20:47
Nový
Zmiznutie lokálnej premennej x vo funkcii zpracuj?
_ 22. 8. 2009 22:12
Nový
└ 
Re: Zmiznutie lokálnej premennej x vo funkcii zpracuj?
_ 22. 8. 2009 22:23
Nový
 
└ 
Re: Zmiznutie lokálnej premennej x vo funkcii zpracuj?
Michal Augustýn 22. 8. 2009 23:02
Nový
 
 
└ 
Re: Zmiznutie lokálnej premennej x vo funkcii zpracuj?
_ 22. 8. 2009 23:49
Nový
terminologie
t42 23. 8. 2009 16:54
Nový
└ 
Re: terminologie
Vít Šesták 23. 8. 2009 17:05
Nový
 
└ 
Re: terminologie
_ 23. 8. 2009 17:35
Nový
Existencia premennej counter v dobe definovania ovládačov
_ 23. 8. 2009 19:04
Nový
A co let?
Michal Augustýn 24. 8. 2009 21:24
Nový
└ 
Re: A co let?
_ 24. 8. 2009 22:03
Nový
 
└ 
Re: A co let?
Michal Augustýn 24. 8. 2009 22:45
Nový
 
 
└ 
Re: A co let?
_ 25. 8. 2009 08:32
Nový
Re: Javascript a oblast působnosti proměnných - díl třetí
Dan 18. 10. 2010 13:00
Nový
       

Tento text je již více než dva měsíce starý. Chcete-li na něj reagovat v diskusi, pravděpodobně vám již nikdo neodpoví. Pro řešení aktuálních problémů doporučujeme využít naše diskusní fórum.

Zasílat nově přidané příspěvky e-mailem