A Javascript programozási nyelv

Objektum-orientált programozás

Osztályok

JavaScript 2.0

A JavaScript 2.0 jelenleg még csak egy terv a JavaScript nyelv továbbfejlesztésére. Az aktuális JavaScript verziótól lényegesen különbözhet, a távolabbi cél azonban az itt lefektetett irányokba való haladás, a különböző szolgáltatásoknak a böngészők újabb verzióiba ad hoc módon való beillesztése helyett. A JavaScript 2.0 fejlesztését az ECMA TC39TG1 munkacsoport erősen koordinálta, felügyelte. A JavaScript szorosan követi az ECMAScript Edition 4 szabványt fejlesztés közben. A szándék az volt, hogy a JavaScript 2.0 and ECMAScript Edition 4 hasonló nyelvek legyenek, de a JavaScript 2.0 tartalmazzon ezen felül plussz dolgokat a visszajelzések és tapasztalatok alapján, mielőtt szabványosítanak. A cél alapvetően az, hogy létrejöjjön egy nyelv, amiben lehet modulokat, osztályokat írni. Egyszóval szerepeljenek benne az objektum-orientáltság kellékei. Lehetőleg a nyelv legyen egyszerű, de ugyanakkor biztonságos programokat lehessen benne írni. A nyelv implementációja legyen kicsi, tömör, rugalmas, de ugyanakkor az írt program helyesen müködjön s a "teljesítménye" is megfelelő legyen. Az OOP szempontjából a leglényegesebb újítások itt össze vannak foglalva. (Eredetileg szerepelt volna a JavaScript 2.0-ban pl. egy függvény paraméterezésénél lehessen névvel is hivatkozni a paraméterre és ne kelljen betartani a paramétersorrendet, de ez végül kikerült belőle, ennek oka az volt, hogy a JavaScript 2.0 "kis" nyelv maradjon).

Osztályok

A legérdekesebb különbséget a Javascript 1.5-höz képest a megújult típusfogalom, és az osztályok bevezetése jelenti.
Immár létezik típusellenőrzés, a típus fogalma pedig a megszokott: típusértékek egy halmazát határozza meg, valamint műveleteket ezen a halmazon. Egy típus értékhalmazát szűkíthetjük, így új típusokat hozhatunk létre. Ebben segít az osztály fogalma. Osztályokat a class kulcsszóval definiálhatunk, például:

class C { var x:integer = 3; function m() {return x} function n(x) {return x+4} } var c = new C; c.m(); // returns 3 c.n(7); // returns 11 var f:Function = c.m; // f is a zero-argument function with this bound to c f(); // returns 3 c.x = 8; f(); // returns 8

A példa Waldemar Horwat-tól, a JavaScript 2.0 megalkotójától származik.

Konstruktorok

Konstruktorokat az opcionális constructor attribútummal definiálhatunk. Nem kell megadnunk az attribútumot, ha a létrehozó függvény neve megegyezik az osztályéval. Ez lesz az ún. default konstruktor, mely meghívódik, ha egy példányt a new operátorral hozunk létre. Példa (szintén W.H.-tól):

class C { var a:String; constructor function C(p:String) {this.a = "New "+p} // The attribute constructor is optional here constructor function make(p:String) {this.a = "Make "+p} static function obtain(p:String):C {return new C(p)} } var c:C = new C("one"); var d:C = C.C("two"); var e:C = C.make("three"); var f:C = C.obtain("four"); c.a; // Returns "New one" d.a; // Returns "New two" e.a; // Returns "Make three" f.a; // Returns "New four"

Lehetőségünk van statikus metódusokkal konstruktorokat szimulálni: készítsünk egy statikus metódust, melynek neve megegyezik az osztályéval, és egy példányt ad vissza az osztályból. Ekkor ez a metódus konstruktorként fog viselkedni, azaz a new operátor ezt fogja meghívni. A statikus metódusnak természetesen nem kell új példányt létrehoznia, hanem visszaadhat egy már létező példányt.

Ősosztály konstruktorának hívása

Legyen a C osztály ősosztálya a B. C minden n konstruktorának tartalmaznia kell hívást a B vagy a C valamely konstruktorára, még mielőtt elérné a this mutatót, vagy befejeződne. A hívás lehet implicit is: ha valamelyik végrehajtási ágban nem adunk meg ilyen hívást, a fordító generálja azt az első utasítás elé, B.B formában. (Azaz az ősosztály default konstruktorát hívja meg.) Az explicit hívásnak a következő formái megengedettek:

super(argumentumok) B default konstruktorát, B.B-t hívja.
super.m(argumentumok) B.m - et hívja, ahol m B valamely konstruktora.
this(argumentumok) C.C-t hívja.
this.m( argumentumok ) C.m-et hívja, m C-nek egy másik konstruktora.

(Egy igazán zárójeles megjegyzés: hivatkozhatunk az adott osztály egy másik konstruktorára is, és az akár, egy harmadikra, de a lánc végén kell lennie egynek, amelyik az ősosztály valamelyik konstruktorára hivatkozik, mert a "körbehivatkozás"-nak nincs értelme. Az viszont igaz, hogy a"C.m kisebb C.n-nél, ha C.n meghívja C.m-et" reláció a várakozással ellentétben NEM részbenrendezés C konstruktorain, sőt ezek a bizonyos láncok, melyeket a részbenrendezés automatikusan adna, sem alakulnak ki feltétlenül.)

Ezeknek a hívásoknak teljes utasításoknak kell lenniük, nem lehetnek egy kifejezés belsejében.
Amint az a szintaxisból is sejthető, nincs lehetőség az öröklődési fában egynél több szintet ugrani felfelé, azaz csak a közvetlen ős konstruktoraira hivatkozhatunk.

Adattagok beállító és lekérdező függvényei

Ha egy osztályban deklarálunk egy változót, ahhoz automatikusan létrejön egy beállító és egy lekérdezo függvény. Ezek végzik a változó beállítását és lekérdezését. Ezek láthatósága alapesetben megegyezik a változóéval, de külön módosíthatjuk azt. Például egy private változó lekérdező függvényét publikussá tehetjük, így a változó az olvasás szempontjából publikusan viselkedik, azaz jobbértékként más osztályokban is használható:

private var name:String; public export get name;

A leszármazott osztály felüldefiniálhatja az ősosztály változóinak lekérdező és beállító függvényeit, amennyiben a változó nem final-ként van deklarálva. Mivel a final, a változók alapértelmezett attribútuma, a virtual kulcsszót kell használnunk az ősosztályban:

class C { virtual var x:integer; var y:integer; } class D extends C { override function set x(a:integer):integer {y = a*2} } var c = new C; c.x = 5; c.x; // Returns 5 c.y; // Returns NaN (the default value for an integer variable) var d = new D; d.x = 5; d.x; // Returns NaN d.y; // Returns 10

(szerzői) Megj.: ez egy nagyon érdekes, és időnként hasznos lehetőség, azonban számomra érthetetlen, hogy miért a final az alapértelmezés. Így már az ősosztály írásakor el kell döntenünk, hogy később felül akarjuk-e definiálni ezeket a függvényeket. Ha elfelejtjük kiírni a virtual-t, később kellemetlenségünk lehet belőle(vagy másoknak). Csak a hatékonyságot találtam, mint e megoldás mellett szóló érvet, de ez egy ilyen dinamikus script nyelvnél szerintem nem elegendő.

Objektumok

JavaScript 1.5

A JavaScript(1.5) prototípusokon alapuló objektum orientált nyelv, ellentétben a sokkal általánosabb osztály alapúval. Arról lehetne vitát folytatni, hogy egyáltalán objektum orientáltnak tekintjük-e, vagy csak objektumokon alapuló nyelvnek, de figyelembe véve, hogy itt egy script nyelvről van szó, akkor a klasszikus objektum fogalom már igen nehezen jöhet szóba.

Az osztály alapú nyelveknél, mint például a Java vagy a C++, külön van választva az osztály és a példány fogalma. Az osztály egy absztrakt dolog, aminek egy konkrét előfordulása a példány. A prototípus alapú nyelveknél, mint például a JavaScript, nincs meg ez a különbség, csak objektumok vannak. Egy objektumot lehet használni, mint egy új objektum template -je. Bármely objektumnak lehet definiálni a tulajdonságait (a továbbiakban property) a létrehozásakor, vagy futási időben.

Az osztály alapú nyelvekben egy elkülönített úgynevezett osztály definiciós részben adjuk meg az osztály metódusait. Itt adhatunk meg egy speciális metódust, amit a példányok létrehozására használunk. Ez az ún. konstruktor. Ebben az eljárásban adhatjuk meg a property -k (mezők) kezdőértékét, és hajhatjuk végre az egyéb létrehozáskor szükséges műveleteket. A new operátor ezen metódusok segítségével hoz létre példányokat. A JavaScript hasonló példát követ, csak nincs elkülönítve az osztálydefinició a konstruktortól. Helyette definiálni kell egy létrehozó függvényt ( function ), ami meghatározza a property -k kezdeti halmazát, és értékeit. Bármely JavaScript függvény lehet konstruktor. Emellett objektumokat létrehozhatunk implicit módon is, azaz nem kezdetben adjuk meg tulajdonságait, hanem már létrejöttük után.

Az osztály alapú nyelvekben az osztályhierarchiát az osztály definicióban kell megadni, azáltal, hogy megmondom, hogy egy alosztály melyik osztaly(ok)tól származzon. JavaScriptben az objektumok létrehozásánál adhatok meg, egy másik objektumot, mint prototípust, aminek következményeként örökli a property -jeit.

Az osztály alapú nyelvekben az osztály fordítási időben jön létre, míg a példányok létrejöhetnek fordítási és futási időben is. Nem lehet megváltoztatni a property -k (mezők) számát vagy típusát miután létre lettek hozva. A JavaScript-ben futási időben lehet hozzáadni vagy törölni propery -ket. Ha például egy olyan objektumnak adok egy új property -t, aminek vannak leszármazottai, azok is meg fogják kapni.

Az osztály alapú (Java) és a prototípus alapú (JavaScript) objektum rendszerek összehasonlítása :

Class-based (Java) Prototype-based (JavaScript)
Az osztály és a példány két külön dolog. Minden objektum példány.
Egy osztályt meghatározni az osztálydefinicióval lehet. Példányokat létrehozni pedig a construktor-ral. Definiálni és létrehozni objektumokat a létrehozó függvénnyel lehet.
Egy egyszerű objektumot a new operátorral lehet létrehozni. Hasonlóan.
Egy objektum hierarchiát az osztálydefinicónál lehet létrehozni, amikor megadjuk egy osztálynak, hogy ő alosztálya egy már létező osztálynak. Egy objektum hierarchiát azáltal hozhatok létre, hogy kijelölök egy objektumot, mint prototípust, társítva a létrehozó függvény mellé.
A property-k öröklődnek az osztálylánc mentén. A property-k öröklődnek a prototípus lánc mentén.
Nincs lehetőség a property-k dinamikus változtatására. A létrehozó funkció csak a property-k kezdő halmazát definiálja. Dinamikusan hozzáadhatunk, vagy elvehetünk property-ket egy különálló objektumból, vagy egy egész objektumhalmazból.
Objektumok létrehozása

Objektumokat több módon is létre tudunk hozni, a céljainktól függ, hogy melyiket választjuk:

Objektumok használata

Az objektum tulajdonságait elérhetjük objektumnév.tulajdonságnév módon. Mivel az objektumok kezelése hasonlít a tömbök kezeléséhez, a nyelv megengedi a tömbelemként történő hivatkozást is: objektum[tulajdonság]. Sőt, hivatkozhatunk a tulajdonságokra indexekkel is, ekkor az egyes tulajdonságok 0-tól szerepelnek a deklarálásnak megfelelő sorrendben. Továbbá a for .. in ciklus alkalmas objektumok tulajdonságainak bejárására. Az Objekt.keys() metódus a tulajdonságok neveit adja vissza, nem az értéküket. Az Object.getOwnPropertyNames() metódus csak a saját tulajdonságot neveit adja vissza, az örökölteket nem! Például:

function suzuki() { this.type = "Suzuki Swift"; this.year = "2000"; } suzuki.type; // értéke "Suzuki Swift" suzuki[0] // értéke "Suzuki Swift" suzuki["year"]; // értéke "2000"

Élő példa:
Láthatóság

Konstruktoros objektum-típus létrehozás esetén lehetőségünk van publikus, privát, és protected láthatósággal rendelkező adattagok felvételére. A this.adattag jelöli a publikus tulajdonságokat és metódusokat, a var kulcsszóval bevezetett adattagokat nevezzük privát, kívülről nem lekérdezhető tulajdonságoknak. A function függvénynév() { .. } szintaktikával felvett objektum metódusok a privát metódusok, és csak a protected metódusok érhetik el az objektum típus privát adattagjait.

function People() { //properties this.impublic = "John Doe"; var imprivate = "private property"; // methods function imPrivate() { return imprivate; } this.imPrivileged = function () { // can call priv. methods return imPrivate(); }; } // public methods People.prototype.imPublic = function () { return this.impublic; }; // static property People.onlyForPeopleObject = "im accessible only from People object"; // prototype property // visszamenőleg a többi People objektum is megkapja People.prototype.forPrototype = "im accessible for all childs";

Élő példa:
Getter, Setter

A getter egy olyan metódus amit egy tulajdonság értékének lekéréséhez használunk. A setter is egy metódus, ezzel egy tulajdonság értékét tudjuk beállítani. Bármilyen olyan beépített, vagy felhasználó által létrehozott objektumnál tudunk definiálni gettert és settert, ahol az utólagos metódus definiálás engedélyezve van.

var gyumolcsok = ["alma","korte","banan"]; // a predefined tömb típusnak nincs size nevű metódusa gyumolcsok.size; // undefined // bővítsük a predefined tömb objektumot egy getter metódussal, és egy setter metódussal var arr = Array.prototype; arr.__defineGetter__("size", function(){ return this.length; }); arr.__defineSetter__("size", function(size){ this.length = size; });

Élő példa:
Tulajdonságok törlése

Objektumok tulajdonságait a delete operátorral törölhetjük, ami nem értékének törlését jelenti, hanem konkrétan az adott attribútum törlését. Ez csak implicit módon létrehozott objektumok esetén használható.

MyObj.c = 7; delete MyObj.c;

Komplex Példa:

A példánkban a következő hierarchiát hozzuk létre.

Hierarchia

Az Employee -nek van két property -je a name (aminek a kezdeti értéke egy üres karaktersor) és a dept (aminek "general" ).
A Manager az Employee -n alapul, de neki van még egy report property -je (aminek a kezdeti értéke egy üres vektor).
A WorkerBee is szintén az Employee -n alapul, és neki egy projects property -je van (aminek a kezdeti értéke szintén egy üres vektor).
A SalesPerson a WorkerBee -n alapul, egy új quota propery -vel (aminek a kezdőértéke 100 ), és felülírja a dept property -t a "sales" értékkel.
Az Engineer is a WorkerBee -n alapul, és neki is van egy új propery -je a machine (aminek a kezdőértéke egy üres karakterlánc), és ő is felülírja a dept property -t, de az "engineering" értékkel.

Most nézzünk egy példát a számtalan lehetőség közül, hogy hogyan lehet ezt megvalósítani.

Hierarchia megvalósítása

Ha pusztán az alap objektum-definiciókat összehasonlítjuk a Java-val igen sok hasonlóságot fogunk tapasztalni.

JavaScript Java
function Employee () { this.name = ""; this.dept = "general"; }
public class Employee { public String name; public String dept; public Employee () { this.name =""; this.dept ="general"; } }

Az igazi különbséget a Manager és a WorkerBee definiciója mutatja meg. JavaScript-ben a prototípus példány meghatározása annyi, hogy értéket adok a prototype propery -ének a létrehozó függvénynek. Ezt a függvény definiciója után bármikor megtehetem. Java-ban meg kell adni az ősosztályt az osztály definiciójában, és ezen kívül már nem is tudok rajta változtatni.

JavaScript Java
function Manager () { this.reports =[]; } Manager.prototype = new Employee; function WorkerBee () { this.projects = []; } WorkerBee.prototype = new Employee;
public class Manager extends Employee { public Employee[] reports; public Manager () { this.reports = new Employee[0]; } } public class WorkerBee extends Employee { public String[] projects; public WorkerBee () { this.projects = new String[0]; } }

Az Engineer és a SalesPerson definiálása hasonló módon történik. A property -k felülírására ( override ) nem kell különösebb trükköt alkalmaznunk,de érdemes odafigyelni rá.

JavaScript Java
function SalesPerson () { 
	this.dept = "sales"; 
	this.quota = 100; 
} 

SalesPerson.prototype = new WorkerBee; 

function Engineer () { 
	this.dept = "engineering"; 
	this.machine = ""; 
}

Engineer.prototype = new WorkerBee;
public class SalesPerson extends WorkerBee { 
	public double quota; 
	public SalesPerson () { 
		this.dept = "sales";
		this.quota = 100.0;
	} 
} 
public class Engineer extends WorkerBee { 
	public String machine;
	public Engineer () { 
		this.dept = "engineering";
		this.machine = ""; 
	} 
}

Felhasználva ezeket a definiciókat, létrehozhatunk objektumokat, amik megkapják a megfelelő kezdőértékeket.

Objektum hierarchia

Futási időben hozzáadhatunk egy új property -t bármelyik objektumhoz. Egyszerűen csak hozzá kell rendelni egy értéket.

mark.bonus = 3000;

Most már a mark objektumnak van egy új property -je, de semelyik másik WorkerBee -nek nincs. Hogyha annak az objektumnak adok új property -t, amelyik prototípusként fel lett használva a létrehozó függvénynél, akkor az összes olyan objektum megkapja, aki töle örökli a property -jeit. Például, ha akarok adni egy speciality property -t minden Employee -nak:

Employee.prototype.specialty = "none";

Öröklődés

A JavaScript 2.0 az egyszeres öröklődést támogatja. Az ősosztályt az extends kulcsszóval adhatjuk meg. Az OOP-ben megszokott valamennyi fogalmat megtaláljuk, ezeket most nem részletezzük. Egy metódus felüldefiniálásakor ki kell írnunk az override attribútumot. Erre azért van szükség, hogy a hibásan írt metódusnevekre a fordító figyelmeztethessen bennünket.

Prototípusokon alapuló öröklődés

Először létrehozunk egy FooBase osztályt, aminek lesz egy váltózója, és ennek a változónak lesz getter és sette metódusa, továbbá egy toString metódusa. Ez lesz a szülő osztály.

// Osztály konstruktora: function FooBase(value) { this.value = value; } // setValue() metódus definiálása: FooBase.prototype.setValue = function(value){ this.value = value; return this; } // getValue() metódus definiálása: FooBase.prototype.getValue = function(){ return this.value; } // toString() metódus definiálása: FooBase.prototype.toString = function(){ return this.getValue(); }

Most pedig létrehozunk egy FooChild osztályt, ami megörökli a FooBase nevű osztályunk tulajdonságait.

// Leszármazunk a FooBase osztályból: FooChild.prototype = new FooBase(); // Definiáljuk a FooChild osztályt: function FooChild(){ // Ős konstruktorát meghívjuk: FooBase.apply( this, arguments ); } // Felüldefiniáljuk a toString() metódust: FooChild.prototype.toString = function(){ return 'FooChild value értéke: ' + this.getValue(); }

Most már akár le is tesztelhetjük, hogy miként viselkedik a két osztály toString() metódusa:

myFooBase = new FooBase(0); baseToString = myFooBase.toString(); document.write(baseToString); // "0"-t fog kiírni myFooChild = new FooChild(0); childToString = myFooChild.toString(); document.write(childToString); // "FooChild value értéke: 0"-t fog kiírni
Prototype, prototype-chain

Feljebb olvashattátok, hogy a JavaScript prototypus alapú nyelv, ez azt jelenti, hogy minden objektumnak van egy prototype adattagja ami egy másik objektumra mutat. Ezt a másik objektumot hívjuk a szóbanforgó objektum prototypusának. Az objektum prototípusának is van egy prototypusa, és így tovább. Ezt nevezzük prototype chain-nek, vagy magyarul prototípus láncnak. A lánc végén a null objektumra mutató objektum van.

Amikor létrehozunk egy új objektumot (gyerek) példányosítással (szülő), akkor a gyereknek létrejön egy [[Prototype]] "virtuális" attribútuma amely a szülő objektum prototípusára mutat. Ha a gyerek objektum egy függvényét meghívjuk, akkor a JavaScript először megnézi a saját tulajdonságokat és metódusokat, ha megtalálta a hívott metódust, akkor nem keres tovább, ha nem találta meg a saját tulajdonságok között, akkor a [[Prototype]]-ban keres tovább, vagyis a szülő objektumban. Ezért van az, hogy ha hozzáadunk egy új metódust egy szülő objektum prototípusához, akkor ez a metódus a szülőből már korábban páldányosított objektumok esetén is hívható lesz, ugyanis a gyerekek sikertelen metódus-keresést követően a szülő prototípusában keresik a metódust.

Mint említettük, a [[Prototype]] attribútum virtuális, ám van egy valóságos tulajdonság, amit __proto__ -nak nevezünk, és ami a szülő prototípusára mutat. Példa:

function Programmer(name){ this.name = name; } var john = new Programmer("John Doe"); (john.__proto__ === Programmer.prototype) ? true : false; // true

A Programmer objektumnak is van egy __proto__ attribútuma ami egy másik objektum prototípusával egyenlő, így visszakövethető a prototípus lánc egészen a null objektumig.
function Programmer(name){ this.name = name; } var john = new Programmer("John Doe"); (john.__proto__ === Programmer.prototype) ? true : false; // true (john.__proto__.__proto__ === Object.prototype) ? true : false; // true (john.__proto__.__proto__.__proto__ === null) ? true : false; // true

Polimorfizmus, dinamikus kötés

A JavaScript 2.0 Kidolgozásra Vár

interfészek

A JavaScript 2.0-ban lehetőségünk van interfészek használatára.

Példa:

interface I { function f(int):int }
class B implements I { function f(i:int):int {return i*i} }