Bindingról JavaScriptben
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).
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.
■
Hiánypótló
Köszönöm
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.
Jó cikk és technikailag semmi
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 :)
Keretrendszer
Technikailag jó a cikk,
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.)
OO
inf3rno:
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 ...
Oks. Akkor megpróbálom
Köszi. Rajtam kívűl
Én is szívesen
Azt hiszem hosszabb lesz
Egységteszteléshez
Köszi. Megnéztem QUnit-ot, kb
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.
http://sethgodin.typepad.com/
Tyrael
http://www.youtube.com/watch?
No beküldtem. Úgy döntöttem
Ha megfigyeljük, a
En megfigyeltem, es nem a
foo
hanem aparam
erteket kapja parameterul a buildHandler()Kösz
setTimeout paraméterek
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.
Különbségek
Különbségek
Köztes megoldásnak?
proclub is
Félre ne értsd, nem proclub
Igazából az egész gondolatmenetem azon indult el, hogy nem tudtam az IE különc viselkedéséről:)
gondold tovább
Jogos, futottam pár
A(z egyik) standard megoldás
Köszönet, pont valami