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:


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:

Příklady zobrazené v tomto článku je možné si stáhnout zde: