A JavaScript nyelv egyik központi koncepcióját képviselő objektumok alapvető tulajdonságait mutatja be a fejezet.
JavaScriptben központi szerepet töltenek be az objektumok. Egyrészt azért, mert pár kivételtől eltekintve (pl. vezérlési szerkezetek) minden objektum a nyelvben, másrészt a nyelv egyik alapkoncepciója is ezek köré épül. JavaScriptben ugyanis nincsenek osztályok, amelyek példányaként jelennek meg az objektumok, hanem csupán egyedi objektumok vannak, amelyek valamilyen szinten kapcsolatban lehetnek egymással.
A JavaScript objektumok egyszerű név-érték párok gyűjteményei. Viselkedésük legjobban az asszociatív tömbökére hasonlít. A név tetszőleges azonosító vagy szöveg lehet (ez utóbbi akkor, ha a név nem felel meg az azonosítóval szemben támasztott elvárásoknak), az érték pedig tetszőleges kifejezés. Ha az érték függvény, akkor a név-érték párokat metódusoknak, minden egyéb esetben tulajdonságoknak (ritkábban adattagoknak) hívjuk. Objektumok megadása legegyszerűbben a nyelv literálformájával történhet. Egy objektum adott tulajdonságának értékére hivatkozás a nevének megadásával lehetséges, vagy objektum.név, vagy objektum['név'] formában.
//objektum definiálása var obj = { tulajdonsag: 1, 'ez is tulajdonság': 2, metodus: function () }; ok( typeof obj === 'object', 'objektum jött létre'); //hivatkozás ok( obj.tulajdonsag === 1, 'működik az objektum.nev elérés' ); ok( obj['ez is tulajdonság'] === 2, "működik az objektum['nev'] elérés" );
A JavaScript nyelv hatékonyságának, sokoldalúságának és rugalmasságának egy részét az objektumfilozófiájának köszönheti. Ennek két markáns alappillére van:
Az alábbiakban e két tulajdonságot járjuk körül: először megnézzük, hogy hogyan hozhatók létre egyedi objektumok JavaScriptben és ezeknek a tulajdonságait miképpen lehet kezelni; majd azt vesszük górcső alá, hogy mik ezek a prototípus-objektumok, és hogyan lehet ezekhez kapcsolódni.
Dinamikus objektumok alatt azt értjük, hogy tulajdonságaik (néhány megszorítástól eltekintve) tetszőlegesen módosíthatók, bővíthetők, törölhetők. Az erősen típusos nyelvekkel ellentétben, ahol az adat csak a típusának megfelelő szerkezetű lehet, JavaScriptben futás időben módosíthatóak a tulajdonságok. Ez nagy szabadságot és lehetőséget ad a programozó kezébe, amivel helyesen élve gyorsan és hatékonyan oldhatóak meg más nyelvekben bonyolultabbnak vélt problémák.
//Objektum megadása var obj = { a: 1 }; //Objektum módosítása obj.b = 2; ok( obj.b === 2, 'Az objektum dinamikusan bővíthető' );
E sorok írásakor a JavaScript mögött álló EcmaScript szabvány 5-ös verziója van érvényben. A korábbi 3-as verzióhoz képest az objektumkezelés az újabb verzióban sok elemmel bővült. Az alábbiakban az objektumok kezelésével kapcsolatos ismereteket az 5-ös verzió szerint tekintjük át.
Az új szabvány az objektumok tulajdonságainak finomhangolására nyújt eszközöket. Így például lehetővé válik annak beállítása, hogy egy tulajdonság értéke megváltoztatható-e (writable), felsorolható-e (enumerable) vagy törölhető-e (configurable). Továbbá lehetőség van egy tulajdonság értékének lekérdezésére és megváltoztatására függvények segítségével (getter és setter). Ezeket a beállításokat egy ún. tulajdonságleíró objektumon (descriptor) keresztül tudjuk megadni, melynek a következő értékei lehetnek:
var descriptor = { value: "érték", writable: true, enumerable: true, configurable: true, set: function(value) { valami = value}, get: function() { return test } };
Üres objektumot egyszerűen az üres objektumliterállal hozhatunk létre. Az így létrejött objektum prototípus-objektuma az Object.prototype objektum. (Hogy pontosan mik is ezek a prototípus-objektumok, arról a következő fejezetben lesz szó.)
Objektum létrehozása az Object.create(prototípus-objektum) függvény segítségével is lehetséges, amelynek kötelező paraméterként meg kell adni, hogy melyik objektum legyen a létrejövő objektum prototípusa. Ha az üres objektumliterállal kompatibilis objektum létrehozása a cél, akkor ide Object.prototype adandó meg, ha viszont nem szeretnénk prototípus-objektumot megadni, akkor null értéket írjunk a helyére.
//Minta var obj = ; //Teszt ok( typeof obj === 'object', 'Objektum jött létre' ); ok( Object.getPrototypeOf(obj) === Object.prototype, 'A prototípus az Object.prototype' ); //Üres objektumliterállal kompatibilis objektum létrehozása var obj = Object.create(Object.prototype); //Teszt ok( typeof obj === 'object', 'Objektum jött létre' ); ok( Object.getPrototypeOf(obj)_ === Object.prototype, 'A prototípus objektum az Object.prototype' ); //Prototípus nélküli objektum létrehozása var obj = Object.create(null); //Teszt ok( typeof obj === 'object', 'Objektum jött létre' ); ok( Object.getPrototypeOf(obj) === null, 'Nincsen prototype objektuma' );
Egy objektumnak már létrehozása pillanatában megadhatunk név-érték párokat, amelyeket természetesen később tetszőlegesen módosíthatunk. Ahogy az a fenti bevezetőben már példaként megjelent, a tulajdonságokat név: érték vagy 'név': érték formában kell vesszővel elválasztva felsorolni a zárójelpár között.
A létrehozandó tulajdonságok finomhangolását lehet elvégezni az Object.create(prototype, descriptors) metódus segítségével, ahol második paraméterként lehet megadni az egyes tulajdonságok leíróobjektumait objektumliterál formájában. Az objektumliterállal kompatibilis objektum megadásához a configurable, enumerable és writable attribútumok igazra állítása szükséges, ugyanis ezek explicit beállítás híján alapértelmezetten hamisak. Az alábbi példán látható, hogy ez a fajta megadás bár rugalmasabb beállításokat tesz lehetővé, mégis sokkal bőbeszédűbb, mint az egyszerű objektumliterálos forma.
//Minta var obj = { alma: 'piros', 'körte': 'sárga' }; //Teszt ok( obj.alma === 'piros', 'Megvan az alma attr' ); ok( obj['körte'] === 'sarga', 'Megvan a korte attr' ); //Objektumliterállal kompatibilis objektum létrehozása előre megadott tulajdonságokkal var obj = Object.create(Object.prototype, { alma: { value: 'piros', configurable: true, enumerable: true, writable: true }, 'körte': { value: 'sárga', configurable: true, enumerable: true, writable: true } }); ok(obj.alma === 'piros', 'Az alma tulajdonság létrejött'); ok(obj['körte'] === 'sárga', 'A körte tulajdonság létrejött');
Egy JavaScript objektum tulajdonságait futásidőben lehet létrehozni, értékét lekérdezni, módosítani és törölni. Ez a JavaScript objektumok dinamikus jellegéből fakad. Új tulajdonságot hozzáadni egyszerűen úgy lehet, hogy egy eddig az objektumban nem létező névhez rendelünk értéket. Értéket lekérdezni egyszerűen az adott névre hivatkozással lehet. Nem létező névre hivatkozva undefined értéket kapunk. Módosítás akkor történik, ha az objektumban már létező névhez rendelünk értéket. Egy tulajdonságot pedig a delete operátorral lehet az objektumból törölni.
//Objektum megadása var obj = { a: 1, b: 2 }; //Minta: új tulajdonság felvételére obj.c = 3; //vagy obj['c'] = 3; ok( obj.c === 3, 'Az új tulajdonság létrejött' ); //Minta: tulajdonság lekérdezése (olvasás) ok( obj.a === 1, 'obj.név formában történő olvasás' ); ok( obj['a'] === 1, "obj['név'] formában történő olvasás" ); ok( obj.d === undefined, 'Nem létező névre hivatkozva undefined-ot kapunk'); //Minta: tulajdonság értékének módosítása obj.b = 42; //vagy obj['b'] = 42; ok( obj.b === 42, 'A tulajdonság módosítása sikerült' ); //Minta: tulajdonság törlése delete obj.c; //vagy delete obj['c']; ok( obj.c === undefined, 'A tulajdonság törlése sikeres');
A fenti módszereken kívül lehetőség van egy új tulajdonság felvételére, illetve módosítására az Object.defineProperty(obj, név, leíró) metóduson keresztül, amelynek első paramétere a módosítani kívánt objektum, második a tulajdonság neve, harmadik pedig a tulajdonságleíró objektum. Egyszerre több tulajdonság is meghatározható az Object.defineProperties(obj, leírók) függvény segítségével. Egy adott tulajdonság leíróobjektumát az Object.getOwnPropertyDescriptor(obj, név) függvénnyel lehet lekérni.
//Üres objektum létrehozása var obj = Object.create(Object.prototype); //Tulajdonság létrehozása Object.defineProperty(obj, 'a', { value: 1, writable: true, configurable: true, enumerable: true }); //Leíróobjektum lekérdezése var desc = Object.getOwnPropertyDescriptor(obj, 'a'); ok( obj.a === 1, 'Az új tulajdonság létrejött' ); ok( desc.value === 1, 'Az érték a tulajdonságleírón keresztül is elérhető' ); ok( desc.writable === true, 'A writable attribútum helyesen került beállításra' ); ok( desc.configurable === true, 'A configurable attribútum helyesen került beállításra' ); ok( desc.enumerable === true, 'Az enumerable attribútum helyesen került beállításra' ); //Tulajdonság módosítása és új létrehozása egyszerre Object.defineProperties(obj, { a: { value: 42, enumerable: false }, b: { value: 2 } }); var desc = Object.getOwnPropertyDescriptor(obj, 'a'); ok( obj.b === 2, 'Az új tulajdonság (b) létrejött' ); ok( obj.a === 42, 'A meglévő tulajdonság értéke megváltozott' ); ok( desc.writable === true, 'A writable attribútum nem változott' ); ok( desc.configurable === true, 'A configurable attribútum nem változott' ); ok( desc.enumerable === false, 'Az enumerable attribútum megváltozott' );
JavaScriptben számos, az objektum egészét, és nemcsak egy-egy tulajdonságát befolyásoló funkció használatára van lehetőség. Ezek között vannak beállító és lekérdező műveletek.
JavaScriptben az objektumok tartalmazhatnak hivatkozást más objektumokra, így tetszőleges objektumhierarchia alakítható ki. Ezek mellett az explicit hivatkozások mellett azonban JavaScriptben minden objektum automatikusan tartalmaz egy speciális, rejtett hivatkozást egy másik objektumra. Ez a hivatkozott objektum az ún. prototípus-objektum vagy röviden prototípus.
Az alábbi ábra sematikusan mutatja két objektum prototípuson keresztül megvalósuló kapcsolatát: az objektum2-nek az objektum1 szolgál prototípusául.
Egy objektum tehát rejtett módon egy másik objektumra mint prototípus-objektumára mutat. Igen ám, de ennek az objektumnak megint csak van hivatkozása egy másik prototípus-objektumra, és így tovább, amíg egy olyan objektumhoz nem érkezünk, akinek ez a hivatkozása null értékű. Prototípus-objektumoknak az így kialakult láncolatát nevezzük prototípusláncnak.
A prototípus-objektum beállítása csak egy új objektum létrehozásakor lehetséges. Az Object.create(prototípus) metódus első paramétereként szükséges megadni az újonnan létrejövő objektum prototípus-objektumát. Prototípus bármilyen objektum lehet. Egy már létrejött objektum prototípus-objektumát megváltoztatni egyelőre nem lehet (bár vannak ilyen irányú elképzelések az új szabványtervezetben).
A prototípus-objektum lekérdezésére kétféle lehetőség is van:
//A prototípuslánc kialakítása var obj1 = Object.create(Object.prototype); var obj2 = Object.create(obj1); //Tesztek ok( obj2.__proto__ === obj1, 'obj2 prototípusa obj1' ); ok( obj1.__proto__ === Object.prototype, 'obj1 prototípusa Object.prototype' ); ok( obj1.isPrototypeOf(obj2), 'obj1 szerepel obj2 prototípusláncában' ); ok( Object.prototype.isPrototypeOf(obj2), 'Object.prototype szerepel obj2 prototípusláncában' ); ok( Object.getPrototypeOf(obj2) === obj1, 'obj2 prototípusa obj1' );
Bár a klasszikus OOP-ről szóló fejezetben részletesen szólunk róla, itt is érdemes megjegyezni, hogy a prototípus-objektum beállítását ún. konstruktorfüggvényeken keresztül is el lehet végezni. Ha a konstruktorfüggvényt new operátorral hívjuk meg, akkor a létrejövő objektum prototípus-hivatkozása (__proto__) a konstruktorfüggvény prototype tulajdonságában megadott objektumra mutat.
Régebbi böngészők nem támogatják az ES5-ben bevezetett újdonságokat. Ezekben a következőképpen lehet az Object.create() metódust helyettesíteni:
if (!Object.create) { Object.create = function (o) { if (arguments.length > 1) { throw new Error('Object.create implementation' + ' only accepts the first parameter.'); } function F() F.prototype = o; return new F(); }; }
Amikor egy objektum tulajdonságának értékét kérdezzük le, akkor először az adott objektumban történik a tulajdonság vizsgálata. Ha megvan, akkor az értéke visszaadódik. Ha viszont az adott nevű tulajdonság nem található az objektumban, akkor a prototípusában történik a keresés, ha ott sincs, akkor annak is a prototípusában, stb. Az adott nevű tulajdonság keresése tehát a kiindulási objektumtól kezdve annak prototípusláncában történik. Ha a legfelső szinten sem található, akkor undefined-dal tér vissza a keresés.
//A prototípuslánc létrehozása előre feltöltött objektumokkal var o1 = { a: 1, b: 2 }; var o2 = Object.create(o1); o2.b = 22; o2.c = 3; //Teszt ok( o2.c === 3, 'A tulajdonság az adott objektumon elérhető'); ok( o2.a === 1, 'A tulajdonság a prototípuson elérhető'); ok( typeof o2.toString === 'function', 'A metódus az Object.prototype-on keresztül elérhető'); ok( o2.b === 22, 'A tulajdonság eltakarja a prototípus azonos nevű tulajdonságát');
Az olvasást, azaz a tulajdonságok lekérdezését, úgy is elképzelhetjük, hogy az egyes objektumok átlátszóak, mögötte látszanak a prototípusláncban szereplő egyéb objektumok is. Minden objektum csak azokat a tulajdonságokat takarja el, amely benne megtalálható.
Korábban már láttuk, hogy egy objektum saját tulajdonságainak neveit az Object.keys(obj) vagy az Object.getOwnPropertyNames(obj) metódussal lehet lekérdezni attól függően, hogy az összes vagy csak a felsorolható attribútummal rendelkező tulajdonságokra vagyunk kíváncsiak. Ezek a metódusok egy tulajdonságlistával térnek vissza. Ha azt szeretnénk megtudni, hogy egy tulajdonság az adott objektum sajátja-e, akkor az obj.hasOwnProperty(név) metódust használjuk. Az előző példabeli objektumhierarchián a következő teszteket futtathatjuk:
ok( o2.hasOwnProperty('c') === true, 'A c tulajdonság az o2 objektum sajátja'); ok( o2.hasOwnProperty('a') === false, 'Az a tulajdonság nem az o2 objektum sajátja');
Az Object.keys(obj) vagy az Object.getOwnPropertyNames(obj) metódussal lehetővé válik a tulajdonságok lekérdezése, de egyrészt ezek csak az adott objektum saját tulajdonságait sorolják fel, másrészt a lekérdezés után megkapott tömbön külön végig kell iterálni. A JavaScript for..in ciklusát éppen objektumok tulajdonságainak iteratív felsorolására találták ki. Jellemzője, hogy csak a felsorolható tulajdonságokat adja vissza, és az egész prototípusláncot végigjárja. Ha csak az adott objektum tulajdonságaira van szükségünk, akkor a felsorolást az obj.hasOwnProperty(név) metódus segítségével szűrhetjük. Az előző példán ez a következőképpen néz ki:
//A prototípuslánc összes felsorolható tulajdonsága var arrForIn = []; for (var i in o2) { arrForIn.push(i); } ok( arrForIn.sort().join(',') === 'a,b,c', 'A for..in ciklus bejárja a prototípusláncot'); //Az adott objektum tulajdonságai var arrForInOwn = []; for (var i in o2) { if (o2.hasOwnProperty(i)) { arrForInOwn.push(i); } } ok( arrForInOwn.sort().join(',') === 'b,c', 'A for..in ciklus hasOwnProperty-vel szűrve'); //Saját felsorolható tulajdonságok var arrKeys = []; Object.keys(o2).forEach(function (i) { arrKeys.push(i); }); ok( arrKeys.sort().join(',') === 'b,c', 'Felsorolás Object.keys-zel');
Az írás, azaz egy objektum tulajdonságának megadása másképpen történik: mindig az adott objektumban kerül felvételre vagy módosításra az adott nevű tulajdonság, függetlenül attól, hogy a prototípusláncban feljebb található-e ugyanolyan nevű tulajdonság.
//A prototípuslánc létrehozása előre feltöltött objektumokkal var o1 = { a: 1, b: 2 }; var o2 = Object.create(o1); //Teszt ok( o2.b === 2, 'Módosítás előtt: érték o1-ből'); ok( o2.hasOwnProperty('b') === false, 'Módosítás előtt: o2-nek nincs b tulajdonsága'); //Írás o2-be o2.b = 42; //Teszt ok( o2.b === 42, 'Módosítás után: o2.b értéke megváltozott'); ok( o2.hasOwnProperty('b') === true, 'Módosítás után: o2-ben létrejött a b tulajdonság'); ok( o1.b === 2, 'Módosítás után: o1.b változatlan');
JavaScriptben minden objektum automatikusan tartalmaz egy belső hivatkozást egy másik objektumra, amit prototípus-objektumnak hívunk. A prototípus-objektumok hosszú hivatkozási sorozatot alkothatnak, ez a prototípuslánc. Tulajdonság lekérdezésekor (olvasáskor) a keresés a hivatkozott objektumtól indul végig a prototípusláncon keresztül, amíg vagy a keresett nevű tulajdonság meg nem lesz, vagy a prototípuslánc tetejét éri el. Így a hivatkozott objektumon keresztül elérhetjük a prototípusláncban felette álló tulajdonságokat is mindaddig, amíg egy azonos nevű tulajdonság a prototípusláncban lejjebb el nem takarja. Úgy lehet elképzelni ezeket, mint átlátszó rétegeket egymáson. A prototípusláncot kizárólag lekérdezéskor (olvasáskor) használja a nyelv, tulajdonság beállítása mindig a hivatkozott objektumon történik, eltakarva így a prototípusláncban feljebb elhelyezkedő azonos nevű tulajdonságokat. A prototípusláncban feljebb elhelyezkedő objektumok tulajdonságait csak úgy tudjuk megváltoztatni, ha közvetlen referenciát szerzünk azokra az objektumokra.
A prototípusláncnak számos hasznos következménye van, amely a nyelv sokoldalúságához és rugalmasságához járul. Ezekről lesz szó a következő fejezetekben.
A JavaScript tömb adattípusa egy magas-szintű, lista jellegű objektum. Utóbbi azt is jelenti, hogy a nyelv nem rendelkezik klasszikus tömb adattípussal, csupán egy speciális, könnyebben iterálható objektummal. A típust általában a [] literálformával deklaráljuk. A kapcsos zárójelek közé írt értékek a tömb alapértékei lesznek.
Minden tömb számokkal indexelt (0-tól kezdődően) — új elemet pedig a push metódussal, vagy az utolsó index utáni explicit értékadással vehetünk fel. A tömb értéke bármilyen adattípus lehet, így nem csak szám vagy sztring, hanem pl. objektum, egy másik tömb vagy pedig egy függvény is. A tömb hosszát a length adattag tartalmazza.
var simpleArray = ['A String']; simpleArray.push(1); simpleArray[simpleArray.length] = ; assertEquals(['A String', 1, ], simpleArray); assertEquals(3, simpleArray.length); // Egy tömbben legegyszerűbben az indexOf metódussal kereshetünk, // amely -1 értéket ad vissza, ha nem találja meg a keresett elemet: assertEquals(0, simpleArray.indexOf('A String')); assertEquals(1, simpleArray.indexOf(1)); assertEquals(-1, simpleArray.indexOf('NotExists'));
A tömbök — néhány sajátosságuktól eltekintve — hagyományos objektumok, így bármilyen adattaggal, metódussal bővíthetjük őket futási időben, azonban a listaként indexelt értékeket speciálisan kezelik. Ha egy szám kulcsot adunk egy tömbhöz, akkor azt automatikusan a listaértékei közé veszi fel — később ezeken képes végigiterálni vagy a metódusait futtatni.
var loopTest = [1,2,3], result = []; loopTest.notAnArrayValue = 'SomeValue'; for (var i=0; i<loopTest.length; i++) { result.push(loopTest[i]); } assertEquals(3, loopTest.length); assertEquals('SomeValue', loopTest.notAnArrayValue); assertEquals([1,2,3], result); assertEquals(3, result.length); assertEquals(undefined, result.notAnArrayValue);
Az előbbi tulajdonság implikálja azt is, hogy kézzel akármilyen indexre írhatunk értéket. Ezzel azonban mindig vigyázzunk, ugyanis két index között a JavaScript automatikusan minden köztes elemet felvesz, undefined értékkel:
var trickyArray = [1,2]; trickyArray[5] = 3; assertEquals([1,2,undefined,undefined,undefined,3], trickyArray); assertEquals(6, trickyArray.length);
A fenti probléma elkerülésére — amikor csak lehet — használjuk a push metódust elemek felvételére. Ha mindenképp kézzel szeretnénk indexeket állítani, akkor valószínűleg nem a tömb adattípusra van szükségünk, hanem a hagyományos objektumra.
A tömb konstruktorfüggvénye az Array, amely számos hasznos, listaműveleteket megkönnyítő metódussal ruházza fel a tömböket.
Az úgynevezett collection műveletek közös tulajdonsága, hogy végigiterálnak a tömbön, és a paraméterül kapott függvényt minden lépésben meghívják az adott elemre:
var numbers = [1,2,3]; // A forEach egyszerűen végigiterál az összes elemen var minimumValue = Number.MAX_VALUE; numbers.forEach(function(item) { if (item < minimumValue) { minimumValue = item; } }); assertEquals(1, minimumValue); // A map az átadott függvény visszatérési értékeiből // egy új tömböt épít var multiplied = numbers.map(function(item) { return item * 2; }); assertEquals([2,4,6], multiplied); // A filter az átadott függvény visszatérési értékei alapján // épít fel egy új tömböt var smallNumbers = numbers.filter(function(item) { if (item < 3) { return true; } return false; }); assertEquals([1,2], smallNumbers); // Az every eldönti, hogy a tömb összes eleme megfelel-e az átadott // metódus igazságvizsgálatára var isNumbers = numbers.every(function(item) { return typeof item === 'number'; }); assertEquals(true, isNumbers); // Az every-hez hasonló some esetén elég egyetlen igaz visszatérési // érték is, a végeredmény igaz lesz var hasThree = numbers.some(function(item) { return item === 3; }); assertEquals(true, hasThree); // A reduce egy értéket állít elő az iteráció végén. // Az előállítandó értéket a második paraméterben inicializálhatjuk, // minden iterációs lépésnek átadja az eddig kiszámolt értékeket // és az aktuális elemet. A visszatérési érték lesz átadva a // következő hívásnak var sum = numbers.reduce(function(previous, item) { return previous + item; }, 0); assertEquals(6, sum);
A JavaScript különböző tömbök között is biztosít műveleteket, így pl. szét tudunk választani egy tömböt többre vagy éppen fordítva, többől tudunk egyet létrehozni.
var numbers = [1,2,3], characters = ['a', 'b', 'c']; // A slice az adott indextől visszaadja egy tömb értékeit assertEquals([2,3], numbers.slice(1)); // Második paramétere egy vég-indexet jelöl, amíg kiszedheti // a szükséges értékeket assertEquals([2], numbers.slice(1,2)); // A concat két tömböt konkatenál össsze assertEquals([1, 2, 3, "a", "b", "c"], numbers.concat(characters)); // Tömbből sztringgé alakíthatunk az elemeket egy adott karakterrel // "összeragasztó" join metódussal assertEquals('a,b,c', characters.join(',')); // Utóbbi fordítottja a split, amely sztringből tömböt alkot assertEquals(characters, 'a,b,c'.split(','));
A tananyag az ELTE - PPKE informatika tananyagfejlesztési projekt (TÁMOP-4.1.2.A/1-11/1-2011-0052) keretében valósult meg.
A tananyag elkészítéséhez az ELTESCORM keretrendszert használtuk.