ugrás a tartalomhoz

Bindingról JavaScriptben

janoszen · 2010. Szep. 9. (Cs), 09.27

Amikor egy javascriptes szembe kerül mondjuk egy event handlerrel, aminek plusz paramétereket szeretne átadni, és még nem ismerkedett meg mélyebben a nyelvvel, általában szomorú lesz. Ilyenkor menekülnek az emberek a mindenféle keretrendszerek védelmébe, amik megvédik a gonosz JavaScripttől. Pedig nem is annyira gonosz az. Nézzük csak.

Javaslom, hogy az itt bemutatott kódokat a könnyebb megértés érdekében másold be egy üres HTML fájlba.

Átadható függvények

Aki a PHP felől jött, annak bizonyára meghökkentő az a tény, hogy a JavaScriptben nem csak értékeket, hanem függvényeket is át lehet adni. Lássuk a példát:


var myFunc = function() {
	alert("Hi!");
};

myFunc();

JavaScriptben a függvények ún. first-class objektumok: futás időben létrehozhatók; átadhatók másik függvény argumentumaként; lehetnek függvények visszatérési értékei; stb.

A példában nem a függvény visszatérési értékét adjuk vissza, hanem magát a függvényt, amit később meg lehet hívni. Ugyaezt lehet hasznosítani többek között a setTimeout() és az addEventListener() függvényeknél:


var myFunc = function() {
	alert("Esemény van!");
};

/* 10 másodperc múlva futtassuk le a myFuncot. */
setTimeout(myFunc, 10 *1000);

/* body-ra kattintáskor is. */
document.getElementsByTagName("body")[0].addEventListener("click", myFunc, false);

A mintakódban a myFunc változónak adunk át egy anoním függvényt, amit utána meg tud hívni a timeout és a kattintás kezelő. Ugyanakkor ez a kód nem működne, mert a myFunc visszatérési értékét használja:


setTimeout(myFunc(), 10000);

Magyarázat: a myFunc viszatérési értéke nem definiált, tehát nem függvény, amit végre lehetne hajtani.

Láthatóság (scope)

A fenti kódnak van egy mellékhatása:


var foo = "test<br>\n";
var myFunc = function() {
	document.write(foo);
};

myFunc();
foo = "test2<br>\n";
myFunc();

Ez a kód a következőt fogja kiírni:


test
test2

Az anonim függvény tehát kilát a fölötte levő (ez esetben globális) névtérbe és futáskor meg is találja a megfelelő változót.

Találós kérdés: mi történik, ha két anoním függvény van egymásba ágyazva? (Megoldás: a belső függvény mindaddig kilát a fölöttes névtérbe, amíg azt egy adott szinten helyi változó el nem fedi.)

Binding

Namost, mi van akkor, ha szeretnénk egy timeoutnak paramétert adni? A függvényt nem hívhatjuk meg, marad tehát a globális változó:


var foo = 0;

var myFunc = function() {
	alert(foo);
};

setTimeout(myFunc, 10000);

Ez ugyan elsőre jó ötletnek tűnhet, de nagyon gyorsan eljön az igazság órája, amikor dinamikus mennyiségű event handlert kell föltenni. Gondoljunk példának okáért egy rotálandó bannerre, amit JavaScriptből kell animálni. Mivel nem ismerjük a bannerek számát, ezért nem tudjuk a megfelelő változókat előállítani. Ugyan lehet tömbbel próbálkozni, de ez finoman szólva is szuboptimális.

Szerencsére van megoldás. Nézzük a következő konstrukciót:


function buildHandler (param) {
	return function() {
		alert(param);
	};
}

var param = 0;
setTimeout(buildHandler(param), 100);
param = 1;
setTimeout(buildHandler(param), 200);
param = 2;
setTimeout(buildHandler(param), 300);

Ez szépen földobálja az ablakokat sorban annak ellenére, hogy a param változó értéke mindjárt az elején megváltozott. Miért is működik ez?

Ha megfigyeljük, a buildHandler() függvény meghívódik (!) és paraméterül kapja a param aktuális értékét. A buildHandler() fogja visszaadni azt a függvényt, ami végül az esemény kezelője lesz. A névtelen függvényünk viszont kilát abba a függvénybe, amiben őt létrehozták, ahol a param változó viszont helyi (tehát nem globális) változó volt egy meghatározott értékkel.

A névtelen függvény tehát abba a scope-ba lát, ahol őt létrehozták (lexical scoping).

A névtelen függvény és a closure általában kéz a kézben járnak, de két különböző dolgot takarnak. A névtelen függvény a function() operátorral létrehozott függvény, tipikusan nincs kötése. Closure-ról akkor beszélünk, amikor az f függvényben deklarált g függvény f lefutása után is hozzáfér g tulajdonságaihoz (lexikai zárvány). Gyakran a g függvény névtelen, de ez nem szükséges feltétel a closure létrejöttéhez. Lásd még a Javascript Closures c. cikket és JavaScript és closure 5 percben fóliákat. (Török Gábor)

A this változó

A JavaScript lehetőségei ennyiben nem merülnek ki, teljes nyugalommal fölülírhatjuk a végrehajtási környezetet (execution context) is. Erre szolgál az apply() metódus.

Gábor hívta fel a figyelmemet arra, hogy a this tulajdonképpen nem változó, hanem nyelvi kulcsszó, nem lehet közvetlenül új értéket adni neki, ezért is kell az említett metódust használni.


var foo = {
	bar: function() {
		alert(this.baz);
	},
	baz: "bumburnyak"
};
var foo2 = {
	baz: "fityfirity"
};

foo.bar();

var baz2 = foo.bar;
baz2.apply(foo2);

Itt a foo.bar() meghívásnál azt kapjuk vissza, hogy "bumburnyak", az apply() metódus után a foo2-ből azt, hogy "fityfirity". Így tehát megváltoztathatjuk a függvény végrehajtási kontextusát.

Kontextus változtatás meghívás nélkül

A fenti példa mindössze azért problémás, mert az apply() metódus egyből meg is hívja a függvényt, ami kevéssé célravezető mindenféle event handleres esetben (setTimeout() és a haverjai). Ehhez segítségünkre lesz a fentebb tárgyalt megoldás a változó kötésekkel.


function buildApply(object, func) {
	return function() {
		func.apply(object);
	};
};

var foo = {
	bar: function() {
		alert(this.baz);
	},
	baz: "bumburnyak"
};
var foo2 = {
	baz: "fityfirity"
};

var baz2 = buildApply(foo2, foo.bar);
alert("Most jon a meghivas:");
baz2();

Na, itt már bonyolódik a dolog. Tehát, átadjuk a buildApply() függvénynek a kontextust, amit szeretnénk fölhúzni, és a függvényt, amit szeretnénk ezzel ellátni. Mivel azon a függvényen belül ezek helyi változók, a létrehozott névtelen függvény látja őket és tudja is használni. Ebben a névtelen függvényben meghívjuk a célfüggvényt az előbb tárgyalt apply() metódussal. Ezt a névtelen függvényt pedig visszaaadjuk és tudjuk használni innentől kezdve.

Performancia problémák

Mivel ezek a nyelvi elemek plusz függvény hívásokat jelentenek, érdemes végig gondolni, valóban van-e szükség a használatukra. A keretrendszerek általában túlzott módon is alkalmazzák őket, ami lehet lassúság oka.

Összefoglalva

A függvények tehát látják az őket létrehozó függvények változóit attól függetlenül, hogy mikor futnak le. A több anoním függvény van egymásba ágyazva, azok kilátnak mindaddig, amíg egy helyi változó azt valahol el nem fedi. Ha egy függvény végrehajtási kontextusát szeretnénk megváltoztatni, akkor azt az apply() metódussal tehetjük meg.

Referencia

A blogbejegyzés megírásában nagy segítségemre volt a Get out of binding situations című íromány. Ezen felül érdemes még elolvasni Jim Ley Javascript Closures jegyzetét. Köszönöm Török Gábor és Ifjú hathatós segítségét a bejegyzés létrejöttében.

Hasonló témában már született írás itt a Weblaboron Funkcionális JavaScript és a modul minta címmel.

 
1

Hiánypótló

Poetro · 2010. Szep. 9. (Cs), 12.11
Szép cikk. Úgy tűnik az ajánlott irodalom megtette hatását, és sikeresen elmerültél a JavaScript nyelv szépségeiben. Már várom a Írjunk alkalmazás szervert PHP-ban! párját node.js-ben :).
2

Köszönöm

janoszen · 2010. Szep. 9. (Cs), 12.32
Köszönöm szépen. Kicsit kényszer hatására lépett előrébb a prioritási listán a tanulás, ugyanis van most egy projekt, amiben szűkösen vagyunk és a sebességi szempontok miatt nem akartunk sok tíz kB-os frameworköt használni, viszont custom cuccokra is szükségünk lett. Azért azt meg kell jegyezzem, hogy Török Gábor lektorálása sokat javított a cikken.

Ami a node.js-t illeti, kicsit kívül esik most a használt technológiák körén, ugyanis a cikkek gyakorlati projektekből szoktak születni. Ettől függetlenül ha földobsz valami demoprojektet, amit meg lehetne valósítani benne, akkor megpróbálok rá időt szakítani.
3

Jó cikk és technikailag semmi

virág · 2010. Szep. 9. (Cs), 14.21
Jó cikk és technikailag semmi kivetnivalót nem találok benne :) annak ellenére, hogy nem mindennel értek egyet ami a cikkedben elhangzott, de ezek főleg szemléletbeli dolgok, a lényeg a hasznosság és aki szeretné megismerni mélyebben a JS-t annak tuti hasznos ez a cikk.
Csak annyit jegyeznék meg, hogy a teljesítmény mindig nagyon fontos szempont, de a módszeresség és a következetesség is, ahhoz, hogy valaki egy nagyobb projekten belül ki tudjon váltani egy komplex és érett keretrendszert úgy, hogy 1 év múlva is ki tudjanak igazodni a kódjában, nagyon módszeresnek és következetesnek kell lennie. Én még nem nagyon láttam ilyesmire példát :) az ellenkezőjére annál inkább...ez olyan mint amikor minden cég saját CMS rendszert fejleszt, de ez már OFF, a cikk jó, ennyi volt a mondandóm :)
4

Keretrendszer

janoszen · 2010. Szep. 9. (Cs), 17.15
Nem is annyira keretrendszer az, mint inkább azt az 5 funkciót, amit JS-ből kell megvalósítani, megírtuk JS-ből. Általános keretrendszert nem jutna eszembe írni.
5

Technikailag jó a cikk,

inf · 2010. Szep. 9. (Cs), 23.29
Technikailag jó a cikk, gratulálok hozzá.

Mivel ezek a nyelvi elemek plusz függvény hívásokat jelentenek, érdemes végig gondolni, valóban van-e szükség a használatukra. A keretrendszerek általában túlzott módon is alkalmazzák őket, ami lehet lassúság oka.
Én nem éreztem úgy eddig még, hogy a plusz néhány függvény hívás lassított volna a dolgokon. A szűk keresztmetszet az eddigi tapasztalataim szerint inkább a DOM manipuláció, és a megjelenítéssel kapcsolatos dolgok voltak. Nyilván most emiatt illesztették bele az újabb böngészőkbe a GPU használatot.

Összességében én a bind-ot, mint Function.prototype.bind nem tartom annyira lényegesnek. Persze az elvet használom, de sokszor nem csak a kontextus átvitelére, hanem más változók átvitelére is.

(Nemrég megvalósítottam egész elfogadható módon a névtér, osztály, öröklődés, típusellenőrzés -es dolgokat js-ben egy saját keretrendszerben. Ez a része olyan 500 sor, de ha van rá érdeklődés, akkor szívesen megosztom, meg írok róla blog bejegyzést vagy egy rövidebb cikket. Nem tudom mennyire izgalmas ez a téma, mert a többség szerintem kész keretrendszert használ, amiben hellyel-közzel megoldották a fenti dolgokat.)
6

OO

zhagyma · 2010. Szep. 10. (P), 08.09
proclub: Szerintem is jó cikk és gratulálok hozzá. A "bumburnyák", "fityfirity" kifejezett feldobott.

inf3rno:
Nemrég megvalósítottam egész elfogadható módon a névtér, osztály, öröklődés, típusellenőrzés -es dolgokat js-ben egy saját keretrendszerben. Ez a része olyan 500 sor, de ha van rá érdeklődés, akkor szívesen megosztom, meg írok róla blog bejegyzést vagy egy rövidebb cikket.

Engem kifejezetten érdekelne az általad megvalósított OO megoldás. Tudom van számtalan keretrendszerben egymástól eltérő vagy hasonló OO támogatás (megnézhető, elemezhető) és volt itt a weblaboron is több diszkurzus a témával kapcsolatban. Én úgy gondolom mindig jöhet egy újabb megközelítés vagy ötletesebb kódrészlet amiből tanulni lehet ...
7

Oks. Akkor megpróbálom

inf · 2010. Szep. 10. (P), 09.51
Oks. Akkor megpróbálom összedobni most a hétvégén.
8

Köszi. Rajtam kívűl

zhagyma · 2010. Szep. 10. (P), 13.32
Köszi. Rajtam kívűl remélhetőleg a "kisebbség" más tagjai is érdeklődni fognak az írásod után.
10

Én is szívesen

Ustak · 2010. Szep. 12. (V), 12.40
mazsoláznám azt az 500 sort, szóval hajrá a cikkel :-)
12

Azt hiszem hosszabb lesz

inf · 2010. Szep. 13. (H), 00.57
Azt hiszem hosszabb lesz megírni, mint gondoltam, alapból gyorsan kellett, aztán megtákoltam. A cikket szeretném UML-el meg TDD alapokon összehozni, viszont ezekben még elég kezdő vagyok. Az UML nagyjából megvan, a TDD-t tanulgatom, egyelőre írtam egy nagyon szimpla tesztelő progit, még agyalok rajta, hogy a tesztekhez azt használjam e, vagy valami test framework-öt.
13

Egységteszteléshez

Török Gábor · 2010. Szep. 13. (H), 10.59
Van jó pár unittest keretrendszer JS-hez, az utóbbi két budapest.js témái között találsz kiindulópontot.
17

Köszi. Megnéztem QUnit-ot, kb

inf · 2010. Szep. 14. (K), 01.49
Köszi.
Megnéztem QUnit-ot, kb én is hasonlót dobtam össze, csak chainelve kellett használni, nem egyesével hívva. Nemtom, hogy az előadáson volt e szó róla, vagy máshol olvastam, hogy QUnit-ot használják a legtöbben. Jobb ha azzal csinálom és nem saját cuccal.
16

http://www.youtube.com/watch?

inf · 2010. Szep. 14. (K), 01.46
23

No beküldtem. Úgy döntöttem

inf · 2010. Szep. 15. (Sze), 00.55
No beküldtem. Úgy döntöttem sorozat lesz belőle, mert nagy a téma, nekem meg nincs időm, meg kedvem sem egyszerre ilyen sokat írni.
9

Ha megfigyeljük, a

hron84 · 2010. Szep. 12. (V), 00.48
Ha megfigyeljük, a buildHandler() függvény meghívódik (!) és paraméterül kapja a foo aktuális értékét.

En megfigyeltem, es nem a foo hanem a param erteket kapja parameterul a buildHandler()
11

Kösz

Török Gábor · 2010. Szep. 12. (V), 16.48
Javítva.
15

setTimeout paraméterek

Garpeer · 2010. Szep. 14. (K), 00.39
Üdv!

Jó kis cikk, egyedül a setTimeout környékén látok némi furcsaságot.
Kicsit nyakatekert a megoldás, mivel - ha jól tudom - lehetséges a következő:
setTimeout(alert, 500, "Ezt fogja kiírni");
tehát a 3. paramétertől kezdve átadja őket a meghívott függvénynek.
18

Különbségek

Poetro · 2010. Szep. 14. (K), 10.14
Ezzel csak az a probléma, hogy a Microsoft implementációjában a 3. paraméter teljesen mást jelent, mint a Mozilla változatában.
19

Különbségek

Garpeer · 2010. Szep. 14. (K), 10.54
Na, megint tanultam valamit (és még egy kicsit jobban szeretem az IE-t...)

Köztes megoldásnak?

setTimeout(function(){alert("szöveg");},300);
20

proclub is

Török Gábor · 2010. Szep. 14. (K), 12.43
Ez jó, de proclub is bemutatta ezt a kódot a Binding résznél. Ebből készített aztán egy rugalmasabb formulát.
21

Félre ne értsd, nem proclub

Garpeer · 2010. Szep. 14. (K), 13.11
Félre ne értsd, nem proclub példájával van bajom, csak egyszerűbbnek tartom, ha nem hozunk létre külön változót a paraméternek, plusz külön nevesített függvényt.
Igazából az egész gondolatmenetem azon indult el, hogy nem tudtam az IE különc viselkedéséről:)
22

gondold tovább

Török Gábor · 2010. Szep. 14. (K), 14.00
Értem, de a példádat gondold tovább: az eseménykezelődet névhez kötöd, mert többször is fel szeretnéd használni, majd use case-től függően át kell adnod neki egy argumentumot, és megérkeztünk proclub mintájához.
25

Jogos, futottam pár

Garpeer · 2010. Szep. 15. (Sze), 20.17
Jogos, futottam pár felesleges kört:)
24

A(z egyik) standard megoldás

tgr · 2010. Szep. 15. (Sze), 07.20
A(z egyik) standard megoldás valami ilyesmi:

Function.prototype.curry = Function.prototype.curry || function() {
    var that = this, args = Array.prototype.slice(arguments);
    return function() {
        return that.apply(null, args);
    }
}

setTimeout(alert.curry("szöveg"), 300);
26

Köszönet, pont valami

Garpeer · 2010. Szep. 18. (Szo), 01.02
Köszönet, pont valami hasonlón gondolkodtam, bár én inkább egy "saját" timeout függvény felé indultam, ami a Mozilla-implementációhoz hasonlóan működik. (Bár valószínűleg ez is erősen konvergál a spanyolviasz újrafeltalálásához)

function timeout(f, t){
  var args=Array.prototype.slice.call(arguments,2);  
  var func = function(){
    return f.apply(null, args);
  }
  return setTimeout(func, t);
}
timeout(alert, 500, "Béla");