ECMAScript 5 – osztályok már(pedig) léteznek
Alábbiakban a júliusi Budapest.js meetupon előadott témámat igyekszem írásban kifejteni. Az foglalkoztatott, hogy hogyan lehetne az objektumorientáltságot jól megvalósítani JavaScript alatt.
Objektumorientáltság
A JavaScript objektumorientált, de nem osztály, hanem prototípus alapú. A kettő közötti lényeges különbséget valahogy úgy tudnám szemléltetni, hogy az osztály egy absztrakt valami, ami konkrét példányban kezd el élni. Mint egy pontosan meghatározott tervrajz, melyet megvalósítva lesz egy autónk. A prototípusos objektumorientáltság ezzel szemben mintapéldányokkal dolgozik: például van egy konkrét, létező cipőnk, elvisszük a cipészhez, hogy szeretnénk egy ilyet, csak legyen magasabb a sarka, és legyen több dísz az oldalán. A JavaScript szemléletét nehéz megérteni, ennek egyik oka, hogy keverve használja az elnevezéseket. Erre nem térnék ki részletesebben, korábban írtam róla.
A this
működése
Jó cikkek születtek a this
-ről, csak a lényeget foglalnám össze: általánosságban véve egy függvény futásakor a this
kulcsszó értéke a függvényhívás előtt álló objektum. Fontos azt látni, hogy a JavaScriptben a függvény külön életet él. Az, hogy neve van, vagy éppen egy objektumhoz tartozik, semmit nem jelent, a kérdés, hogy hogyan hívtuk meg. A következő kódrészletben az alsó blokkban mindenhol ugyanazt a függvényt hívjuk meg, különböző this
értékekkel, ezek vannak a megjegyzésben.
function a() {};
x.b = a;
y.z.q = x.b;
a(); // undefined/global scope
x.b(); // x
y.z.q(); // y.z
Mivel a függvények ilyen könnyen pakolhatóak egyik objektumból a másikba, nagyfokú rugalmasság érhető el. A MicroEvent.js jó példa arra, hogy nem kell származtatni, ha egy gyakran használt funkcionalitással szeretnénk felruházni egy objektumot. Csupán elkészítjük a függvényeket és változókat, és kiterjesztjük vele az eredeti objektumunkat. (Ez a mixin vagy trait programozási paradigma.) Egy egyszerűbb példában egy számlálót valósítunk meg:
var COUNTER = {
_counter: 0,
getNext: function () {
return ++this._counter;
}
};
var myObject = {
number: 42
};
myObject.getNext = COUNTER.getNext;
myObject._counter = COUNTER._counter;
var myObject2 = {
string: 'HELLO'
};
myObject2.getNext = COUNTER.getNext;
myObject2._counter = COUNTER._counter;
Hogyan került a homokszem a gépezetbe?
Ez mind szép és jó lenne, de az a gond, hogy a JavaScriptben csak egyetlen függvénytípus létezik, ez viszont nagyon sokféle formában előfordul. Tegyük fel, hogy egy metódusban AJAX kérést kell indítanunk:
object.method = function () {
this.request({
parameter: this.counter,
callback: function (response) {
this.counter += response.data;
}
});
};
Nem fog menni. Ugyanis a callback
paraméterben megadott valami egy függvény, melynek meghívásakor a this
nem valószínű, hogy az object
-re fog mutatni, hiszen nem úgy hívtuk meg, hogy object.callback()
. De nézhetjük az újonnan bevezetett tömbkezelő függvényeket is. Ha egy tömböt értékeit szeretnénk összegezni egy saját függvény szerint:
object.getSum = function () {
return this.myArray.reduce(function (previous, current) {
return previous + this.getValue(current);
}, 0);
};
És még sorolhatnám a példákat, hiszen van bőven: időtúllépés, egyéb események kezelése.
Mi a megoldás?
Valahogyan különbséget kellene tudnunk tenni a metódusok és az egyéb (kisebb) függvények között. Hogy egy metóduson belül mindig tudjunk hivatkozni az objektumunkra.
_this
Az egyik lehetőség, hogy a függvény futásakor létrehozunk egy változót, ami az aktuális this
-re mutat. Ha ezt a módszert választjuk, célszerű lerögzíteni, és minden függvény elején ezt bevezetni, s onnantól csak ezt használni (és nem az eredeti this
-t), hiszen így a kódrészleteink áthelyezhetőek maradnak, míg ha keverve használjuk a this
-t és a _this
-t, akkor át kell írni, ha valami kikerül egy callbackből. Az előző példáknál maradva:
object.method = function () {
var _this = this;
_this.request({
parameter: _this.counter,
callback: function (response) {
_this.counter += response.data;
}
});
};
object.getSum = function () {
var _this = this;
return _this.myArray.reduce(function (previous, current) {
return previous + _this.getValue(current);
}, 0);
};
Számomra kissé természetellenesnek tűnt, hogy mindig külön meg kell adni az objektumot, de ez is megszokható.
Új nyelvi elem függvény létrehozására
A JavaScriptben a hatókör függvényszintű, és nagyon gyakran kell kiírnunk azt a jó hosszú szót, hogy function
. Ez másoknak sem tetszik, és az eddig tárgyalt problémákkal együtt felmerült, hogy biztosítani kellene a lehetőséget arra, hogy röviden hozzunk létre kisebb függvényeket, és ha ezt megtehetjük, akkor ezek a rövid függvények ne módosítsák a this
értékét. A ECMAScript wiki oldalán a következő javaslat szerepel:
object.method = function () {
this.request({
parameter: this.counter,
callback: #(response) {
this.counter += response.data;
}
});
};
object.getSum = function () {
return this.myArray.reduce(#(previous, current) {
prev + this.getValue(current);
}, 0);
};
Itt hashmarkkal lehet megadni a rövidített függvényt, és nem kell kiírni a return
-t sem. (Ellenvéleményként szerepel, hogy Brendan Eich szintén szimpatizált a hashmarkkal, de ő konstans objektumok létrehozására szerette volna.) Ez még amúgy is csak javaslat, és valószínűleg nem fogják elfogadni.
A SafeJS hasonló módon terjeszti ki a JavaScript lehetőségeit. Itt a @
jel a „this.
”-nak a rövidítése, a #
ugyanúgy a függvény rövidítése, a return
pedig „->
”-ként írható. Itt a #@
kifejezéssel létrehozott függvények őrzik meg a this
értékét.
object.method = function () {
this.request({
parameter: this.counter,
callback: #@(response) {
@counter += response.data;
}
});
};
object.getSum = function () {
return this.myArray.reduce(#@(previous, current) {
-> previous + @getValue(current);
}, 0);
};
Nagyon-nagyon tetszik, amit a SafeJS szeretne megvalósítani, de biztos, hogy nem fogom használni. Abban a szerencsés helyzetben vagyok, hogy munkahelyemen nem kell a böngészőkompatibilitással bajlódni. Lévén, hogy a JavaScript a Mozillánál (pontosabban a Netscape-nél) született és élte korai fejlődését, ők is biztosítanak olyan dolgokat, amik jó lenne, ha benne lennének a mainstreamben. Gondolok itt a let
, const
kulcsszóra, az E4X
-re sít. Önként mondtunk le ezeknek a használatáról (pedig mint ifjú titán én is igyekeztem volna bevezetni), hiszen onnantól kezdve se JSHint, se JSBeautifier.
Ha nem lehet _this
, akkor legyen that
!
De nevezhetném self
-nek, vagy THIS
-nek is, attól függ, ki honnan jött. A fenti problémákkal szembesülve ötlik fel a lelkes JavaScript programozóban a lexikai zárvány használatának a lehetősége:
function Constructor() {
var that = this;
/* … */
that.method = function () {
that.request({
parameter: that.counter,
callback: function (response) {
that.counter += response.data;
}
});
};
that.getSum = function () {
return that.myArray.reduce(function (previous, current) {
return previous + that.getValue(current);
}, 0);
};
return that;
};
var object = new Constructor();
Így az „osztályon” belül a that
mindig az aktuális objektumra mutat. Ráadásul így egyszerűen tudunk objektumhoz kapcsolt metódust átadni:
setTimeout(object.method, 1000);
// és nem kell:
setTimeout(function () {
object.method();
}, 1000);
Ennek is megvan azonban a hátránya: jól tudunk így önálló objektumokat készíteni, lehetnek privát változóink a zárványon belül, de az öröklődés nehezebben megoldott. Kalandra fel, új ismeretünket felhasználva evezzünk kissé mélyebb vizekre!
Osztály vagy nem osztály?
A JavaScriptben nincs osztály, mint nyelvi elem, és nem is szabad így gondolni a konstruktor függvényre. Különben is, amit meg tudunk valósítani, az nagyon messze áll az osztály fogalmától, legalábbis amit én osztálynak tartok. Számomra egy osztálynak nevezett valami a következő tulajdonságokkal rendelkezik:
- Vannak „metódusai” (függvények) és „adattagjai” (változók).
- Van lehetőség ezek láthatóságának a meghatározására (legalább
private
,protected
,public
szinten). - Van definíciós része és működési része. A két kód lehet ugyanott, akár vegyesen, de pontosan meg van határozva a struktúra, és az nem változik a futás közben. Borzalmas, amikor egy 1400 soros objektum 577. sorában érték adódik valami addig nem létező objektum szintű változónak, amit valahol fogunk használni, de igazából nem tudjuk, ki hivatkozik rá, még az is lehet, hogy kívülről is el akarják érni… Az osztály definíciója legyen egyértelmű. Lássam, milyen funkciókat valósít meg, ehhez milyen belső mechanizmust használ, és ne legyenek egyszercsak megjelenő mágikus dolgok benne.
- A láthatóság meghatározása a definíciós lépésben történik, utána nem kell foglalkozni vele. Ha valami
protected
, akkor egyszer mondjam meg róla, hogyprotected
, ne kelljen mindig kitennem elé az aláhúzásjelet. Veletek nem fordult elő, hogy valamilyen korábban privátnak szánt funkciót igényelt az „ügyfél”, és biztosítani kellett a számára? A klasszikus JavaScriptben jelölhetjük a saját dolgokat aláhúzással, ilyen esetben sok helyen át kell írni – vagy nem foglalkozunk vele, és minden aláhúzás nélküli, de akkor nem fogunk tudni kiigazodni a saját kódunkon, nem tudjuk, mibe nyúlhatunk bele, mi az, ami az „API-hoz” tartozik, mit lehet bátran refaktorálni. - Lehessen osztályok közötti hierarchiát, öröklődést megvalósítani.
- Leszármazott osztályban megváltoztathatom valaminek a láthatóságát (például
protected
-rőlpublic
-ra) úgy, hogy az ős osztályt nem kell módosítanom. - Az osztály metódusai „kötve” vannak az osztályhoz, nem élnek külön életet. Az
object.f
ne egy függvény legyen, hanem azobject
f()
függvénye. Akárhol, akármilyen formában hívom meg. - A metódusokat leszármazott osztályban felül lehet definiálni (de úgy, hogy szükség esetén elérhető az ősosztály adott metódusa), az adattagoknak pedig értéket tudok állítani.
- Az osztály példányai ellenőrizhetőek
instanceOf
jellegű vizsgálattal (a leszármazott osztály példányai az ősosztálynak is példányai). - Lehessen konstruktort megadni, amely az adatok inicializálását végzi. Ha a leszármazott osztályban nincs megadva konstruktor, a szülő konstruktora automatikusan hívódjon meg, egyéb esetben kézzel. (A JavaScriptben gondot okoz, ha a szülő konstruktora paramétert vár).
ECMAScript 5 alatt ezek a feltételek teljesíthetőek!
Készítettem egy proof of concept implementációt, amely teljesíti a fenti elvárásokat. A forráskód a GitHubon elérhető, a fentebb említett követelmények tesztként részletesen ki vannak fejtve, a szintakszis miatt néhány részletet kiemelek. Úgy gondolom, nem szükséges bőven kifejtett magyarázat, hiszen pontosan úgy működik, ahogy azt például Javaban megszoktuk.
function SuperClass() {
var that = ES5Class.create(this);
// Definíciós rész, itt kell megadni a láthatóságot,
// utána már csak annyi kell, hogy pl. that.privateVariable
that.private.privateVariable = 'super private';
that.protected.protectedVariable = 'super protected';
that.public.publicVariable = 'super public';
that.private.privateFunction = function () {return 'super private';};
that.protected.protectedFunction = function () {return 'super protected';};
that.public.publicFunction = function () {return 'super public';};
return ES5Class.finalize(that);
}
function SubClass() {
var that = ES5Class.create(this, SuperClass);
return ES5Class.finalize(that);
}
var object = new SubClass();
function MyClass() {
var that = ES5Class.create(this, arguments);
that.protected.v = 2;
that.protected.f = function () {return 2;};
that.protected.fn = null;
that.__constructor__ = function (variable, f) {
that.v = variable;
that.fn = f;
try {
// Methods cannot be changed
that.f = f;
} catch(e) {}
try {
// You can not add new properties here
that.nonExists = 'not work';
} catch(e) {}
};
return ES5Class.finalize(that);
}
function MySubClass() {
var that = ES5Class.create(this, arguments, MyClass);
that.__constructor__ = function (variable) {
that.super.__constructor__(variable + 1, function () {return 4;});
};
return ES5Class.finalize(that);
}
var mySubObject = new MySubClass(3);
Összegzés, tapasztalat, vélemény
A kód ECMAScript 5 alatt működik, ezt minden újabb böngésző támogatja. A jól megírt program sikeres futtatásához elegendő, ha van getter és setter támogatás, ilyenkor nincs ellenőrzés a létrehozás utáni változások elkerülésére. (Egyetlen kivétel a Safari volt, ami az object.super
kifejezésre fenntartott szó hibát dob.)
A projektet sikeresnek tartom, hiszen a kitűzött cél megvalósításra került – a gyakorlatba azonban mégsem került bele, ugyanis az összes többi függvénytár, eszköz és IDE másfajta gondolkozásmódot követ.
A Budapest.js-en is elhangzott az a kérdés, hogy miért akar mindenki osztályt tenni a JavaScriptbe, hiszen ennek a nyelvnek is megvan a maga logikája, szemlélete. A válasz egyszerű: mert bonyolult JavaScriptben JavaScript gondolkodásmóddal programozni – ezzel kezdtem a bevezetést –, hiszen csak egyetlen függvény nyelvi elemünk van, a this
átadogatása meglehetősen nyakatekert módszer. Szerencsére most már van szép megoldás, ez pedig a CoffeeScript, amely arra törekszik, hogy a JavaScript hiányosságain javítson, de a szemléletét megtartsa („The golden rule of CoffeeScript is: »It’s just JavaScript«.”). Leegyszerűsítették a függvények létrehozását, és új nyelvi elemet vezettek be. A ->
megegyezik a JavaScript function
-nal, a =>
használata esetén pedig a függvény belsejében ugyanaz marad a this
, mint ami kívül volt (mintha a fentebb kifejtett _this
módszert használnánk).
Nagyon jó írás, gratulálok,
+ BÚÉK minden Weblaboros olvasónak, fejlesztőnek! :)
Egyetértek. Miért kell
Annak meg nem látom értelmét a mindenféle kódkiegészítők korában, hogy rövidítsük le a függvények nevét #-ra stb.
Miért nem lehetne "csak úgy" elhagyni?
Object literallal is:
A probléma a parser-nél lehet
Az instanceof operátorral nem
instanceof
operátorral nem tudunk úgy sem operálni, mert MISE-ben a DOM Node-ok, és a window sem leszármazottja az Object-nek. Én anno írtam egy kis rendszert, amivel meg lehetett valósítani az overloading-ot, de ugye az MSIE-ben nem volt hasznos, úgy hogy kukába raktam :DHát jah, nekem is volt egy
Sajnos nem értem
( Nekem úgy tűnik, hogy ez maximum valamilyen hierarchia kezeléssel megoldható a parser számára: ha nincs előtte kulcsszó (pl if) akkor a zárójel-kapcsoszárójel kombót értelmezze függvénydeklarációként. )
Ez úgy megy, hogy sorba megy
pl:
function -> függvény következik
name -> függvény neve
( -> paraméter lista kezdete
arg1 -> első paraméter neve
, -> következő paraméter jön
arg2 -> második paraméter neve
) -> vége a paraméter listának
{ -> blokk kezdete
blokk -> blokk belseje
} -> blokk vége
Ezek így szépen sorban mennek. Nem tudom pontosan hogyan, lehet, hogy regex-el, lehet csak karaktereket néznek, passz...
Ami biztos, hogy ahhoz, hogy (...){-ot érzékelni tudja a regex, ahhoz positive lookahead szükséges, ami meg annyira nem mindennapos regex feature...
Persze
Nem is a parser működése a lényeg, hanem a szabvány, aminek megfelelően kell értelmeznie az állományt a parsernek. Eddig nem találtam olyan elemet ami miatt ne lehetne akár el is hagyni a "function" kulcsszót. Persze nem akarom ezt erőltetni, csak úgy eszembe jutott.
Pedig van, javascript nem
a.)
Erről beszéltem én is
Az a=2+3*5 is értelmezhető lenne kétféleképp, ha nem tudnánk, hogy a szorzás "erősebb".
A "zárójel után kapcsos" lehetne a szabvány szerint függvénydeklarációként értelmezve és kész.
Egyébként is az általad írt a.) értelmezés szerint a kapcsos zárójeles rész egy jelöletlen funkciójú blokkot definiál, amiben önmagában nem sok logika van, sőt, a sima zárójeles résznél is felesleges a zárójel (ha jól emlékszem a zárójel után mindenképp kéne legalább egy "új sor", ha nincs pontosvessző). Ergo nyilvánvaló, hogy függvénydefinícióról van (lenne) szó.
kapcsos zárójeles rész egy
Hát én nem szoktam használni, de attól még egy ugyanolyan nyelvi elem, mint az összes többi... :-) Persze tényleg lehetne sorrendet állítani a kettő között, vagy akár kikötni a ; használatát...
Ettől függetlenül én csak arra reagáltam, hogy azt mondtad, hogy nincs olyan nyelvi elem, amivel ütközik. Márpedig van... :-)
Vagyis
Persze, a parser mindent meg
Anno még gondolkodtam valami ilyesmin:
Na jó mégis kéne, mondjuk hashCode az Object.prototype-ba még jó lenne... De több már tényleg nem :D
Én is
Határozottan egyetértek!
Amit a cikk elején írtam, és amit te is idéztél az amiatt van, mert a JavaScript nem támogatja a saját szemléletét, ugyanis nyelvi szinten nem tesz különbséget a függvény (értsd: objektumhoz nem kapcsolódó futtatható nyelvi elem) és a metódus (értsd: objektumhoz kapcsolódó futtatható nyelvi elem) között. Miért mondom ezt?
Ahhoz, hogy a JavaScript objektum orientált világképét megértsük, két dolgot kell világosan látni:
Az osztály alapú objektum orientáltságnál amikor létrehozok egy metódust, az fixen hozzá van kötve az osztályhoz (pontosabban annak példányaihoz egyenként). JavaScriptben a metódus meghívásának pillanatában dől el, hogy éppen melyik objektumhoz kapcsolódik, és ezt az objektumot a metóduson belül a
this
változóval érhetjük el. Ez az, amit nem biztosít a JavaScript, hiszen ha egy metódus belsejében megjelenik valahol afunction
(ez pedig lényegi része a nyelvnek az egyszálúságnak és az aszinkronitásnak köszönhetően), abban a blokkban bár a metóduson belül vagyunk, athis
mégsem arra az objektumra mutat, amihez a metódusunk a meghívás pillanatában kapcsolódik. Önellentmondás. Ez az, ami inspirálta a különféle hack-eléseket (_this
,that
, és hogy az osztály alapú objektum orientáltságot megpróbáljuk valahogyan belevinni).Ennek a szemléletnek a helyes működését biztosítja a CoffeeScript biztosít azáltal, hogy lehetőséget ad metódusok létrehozására (a
->
-lal), és a metódusokon belül függvények használatára (a=>
-lal). Így CoffeeScriptben tudunk igazán JavaScript szemlélettel programozni. (Ezt fejtettem ki az új nyelvi elem fejezetben is, de szerintem azokra nem célszerű építeni a közeljövőben.)ugyanis nyelvi szinten nem
Meg harmadikként még idetenném az osztályt is, mert ugye azt a konstruktor függvényével adjuk meg... Jó sok munkával ez a három elválasztható egymástól.
Egyébként én nem értem, hogy miért foglalták le például a class-et és az extends-et, mint kulcsszót, amikor nem is használják őket...
Nagyon jó volt, tetszett!
( A végén a "mintha a fentebb kifejtett this módszert használnánk" nem inkább that akar lenni? )
nem
this
-t őrzi meg végig a metódus során, mintha a_this
-t használnánk."with statement"
Én szoktam alkalmazni a
with
operátort az osztálymetódusokban, hogy ne keljen százszor ki írni athis
változót, illetve a "use namespace" helyett. Erről mit gondoltok? Azért kérdezem mert ugyan sok helyen nem látom használni awith
operátort de szerintem sokkal könnyebb vele dolgozni mint orba-szájbathis
-t írogatni. Van ennek valami mellékhatása a teljesítményre?Ne használd.
Egyrészt átláthatatlan lesz
Egyrészt új scope-ot hoz
Azt, hogy nem kényelmesebb,
Nincs kód kiegészítőd?
De van, csak most ismerkedem
Szerintem próbálkozz
Szerintem nem rosszul
Én meg pont a fordítottja
Lehet módosítani
Eclipse
Nekem az Eclipse PHP-plugin
Ja, hogy benéztem, és JS-ről volt szó... Bocsi, azért ha valaki tud valami választ a kérdésemre, ne tartsa vissza.
Bár nincs PHP pluginem, de
Preferences->PHP->Code Style->Formatter->Edit
Ugyanazokat a beállításokat tudja, mint a NetBeans.
Formatter alatt ennyi van:
Egy éve mindent eclipse-be
Az vagy eclipse volt, vagy
Rengeteg mindent be lehet
Ohh, ezt benéztem :-) Csak