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:
function example1() {
//this shows no allert
var myFnct = function() { alert("Hello World!") };
//this shows function as string - no execution will happen
alert(myFnct.toString());
//there we'll execute it
myFnct();
}
function example2() {
//we'll fetch a function and execute it on next line
var myFnct = getSomeFunction();
myFnct("Father Fourah");
//we can do it even in shorter way - looks quite ridiculous - doesn't it?
getSomeFunction()("Reader");
}
function getSomeFunction() {
return function(name) {alert("Hello " + name)};
}
function example3() {
var names = ["Jan", "Petr", "Milan"];
var myFnct = function(name) {alert(name)};
forEachExecute(names, myFnct);
//or the same in more compressed way
forEachExecute([1,2,3], function(nmb) {alert(nmb)});
}
function forEachExecute(data, callback) {
for(i = 0; i < data.length; i++) {
callback(data[i]);
}
}
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):
function example4() {
//this is equivalent to anonymous function declaration
var myFnct = someFunction;
//this proves that function isn't executed until next line
alert("Before function execution");
myFnct();
//the same is valid for method with parameters
//we can deliver parameters only when we call method not before
var myFnct2 = someFunctionWithParam;
alert("Before second function execution");
myFnct2("Jan");
//this won't work as it executes function immediately
//and assigns null value to our variable
var myFnct3 = someFunctionWithParam("Petr");
alert("As you can see this is displayed after execution of third function, value "
+ myFnct3 + " is assigned to variable, not a function itself.");
}
function someFunction() {
alert("Hello world!");
}
function someFunctionWithParam(name) {
alert("Hello " + name + "!");
}
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ě
function example5() {
var myFnct = getFunctionExample5("Just kidding.");
//can you see? getFunctionExample5 method scope is closed now
//but we can still access local variable name of that scope via closure
//in example we are mixing even method parameters
myFnct("No, no - I am serious.");
}
function getFunctionExample5(suffix) {
var name = "Father Fourah";
return function(postSuffix) {alert(name + " rulez!\n" + suffix + "\n" + postSuffix)};
}
function example6() {
var myFnct = getFunctionExample6();
//this doesn't work no closure was created
//we have acquired only method pointer
myFnct();
}
function getFunctionExample6() {
var name = "Father Fourah";
return example6function;
}
function example6function() {
alert(name + " See? Name is not know in this case - this doesn't create closure!");
}
function example7() {
var myFnct = getFunctionExample7();
//but in case of inner methods closures are created
myFnct();
}
function getFunctionExample7() {
var name = "Father Fourah";
function example7function() {
alert(name + " can use even inner functions - these are closures!")
}
return example7function;
}
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ě.
function example8() {
var myFnct = getFunctionExample8();
//but closures keep reference to whole stack tree
//not only to stack of method closure is created in
myFnct()();
}
function getFunctionExample8() {
var name = "Father Fourah";
return function() {
var suffix = "goes insane!"
return function() {
alert(name + " " + suffix);
}
}
}
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ě:
function caveat1() {
//this will create array with three closures in it
var myFncts1 = getFunctionSet(1);
var myFncts2 = getFunctionSet(10);
//we'll examine array twice
for(i = 0; i < 2; i++) {
//and we'll call each closure in that array
for(j = 0; j < myFncts1.length; j++) {
//we could expect displaying 1, 2, 4, 4, 5, 10
myFncts1[j]();
}
}
//now again for the second array
for(i = 0; i < 2; i++) {
for(j = 0; j < myFncts2.length; j++) {
//we could expect displaying 10, 11, 22, 22, 23, 46
myFncts2[j]();
}
}
//as you can see, both arrays keeps their own stack
alert(
"Final value of first set is " + myFncts1[3]() + "\n" +
"Final value of second set is " + myFncts2[3]()
);
}
function getFunctionSet(startingValue) {
var number = startingValue;
return [
function() {alert(number)},
function() {number++; alert(number)},
function() {number=number*2; alert(number)},
function() {return number},
];
}
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:
function caveat2() {
var myFnct = getCaveat2function();
//if you think value 1 will be displayed,
//you're terribly wrong - neither 1 or error will occur
myFnct();
//when we need to fix variable values we need to use objects
getCaveat2functionKeepingItsOriginalValue().showNumber();
}
function getCaveat2function() {
var number = 1;
var myFnct = function() {alert(number + anotherNumber)};
number ++;
var anotherNumber = 50;
return myFnct;
}
function getCaveat2functionKeepingItsOriginalValue() {
var number = 1;
//for fixing values we need to create objects
var MyFnct = function(number) {
//this will copy number value at the time
//instance of this object is created
var myNumber = number;
//by this declaration we'll create new method
//of this object that simply displays inner value
this.showNumber = function() {
alert(myNumber)
}
};
//in this moment variables are copied
var result = new MyFnct(number);
//this won't affect result inner value
number++;
return result;
}
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ě:
function caveat3() {
var data = ["Janek","Pepa","Luca"];
var myFncts1 = getLoopFunctionSet(data);
for(var i = 0; i < myFncts1.length; i++) {
//do you think we'll see Closure #Janek, Closure #Pepa, Closure #Luca ?
//nope! we'll see Closure #undefined, Closure #undefined, Closure #undefined !
//why? because variable i value at the end of method getLoopFunctionSet is 3
myFncts1[i]();
}
//but we can solve it with objects pattern
var myFncts2 = getLoopFunctionSetSolution(data);
for(var j = 0; j < myFncts2.length; j++) {
myFncts2[j].showIt();
}
//but we can solve it with extended function pattern
var myFncts3 = getLoopFunctionSetSolution(data);
for(var k = 0; k < myFncts3.length; k++) {
myFncts3[k].showIt();
}
}
//naive method
function getLoopFunctionSet(sourceList) {
var result = new Array(sourceList.length);
for(var i = 0; i < sourceList.length; i++) {
result[i] = function() {alert("Closure #" + sourceList[i])};
}
return result;
}
//data transfer object pattern
function getLoopFunctionSetSolution(sourceList) {
var result = new Array(sourceList.length);
for(var i = 0; i < sourceList.length; i++) {
var Dto = function() {
var innerValue = sourceList[i];
this.showIt = function() {alert("Closure #" + innerValue)};
};
result[i] = new Dto();
}
return result;
}
//function transfer pattern
function getLoopFunctionSetAnotherSolution(sourceList) {
var result = new Array(sourceList.length);
for(var i = 0; i < sourceList.length; i++) {
//calling extendedFunction will enforce javascript to copy value of variable
//on curent position in array
result[i] = extendedFunction(sourceList[i]);
}
return result;
}
function extendedFunction(name) {
return function() {alert("Closure #" + name)};
}
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.