JavaScript standard könyvtárak

Kategória: Web fejlesztésJavaScript.

A nyelvi elemek egy nyelvnek csak az egyik része. Programkönyvtárak nélkül nehéz lenne hasznos dolgot végrehajtani. Lássuk a JavaScript legfontosabb belső könyvtárait!

Típusok

Ebben a részben olyan könyvtárakról lesz szó, amelyek típusokkal kapcsolatosak.

String

A leggyakoribb string kezelő műveletek a JavaScript-ben is meg vannak, sőt, azzal, hogy a HTML szöveg alapú, már kezdettől fogva hangsúlyos. Ennek a szakasznak nem célja a JavaScript string műveleteinek részletes bemutatása; inkább csak egy kis ízelítőt ad a lehetőségek közül. Lássunk először egy rövid példát:

let s = "alma, körte, szilva, barack"
console.log(s.length) // 27
s.split(",").forEach(s => console.log(s.trim())) // külön sorokban a gyümölcsök
console.log(s.search("szilva")) // 13
console.log(s.substring(6, 9)) // kör
console.log(s.replace("szilva", "meggy")) // alma, körte, meggy, barack
console.log(s.toUpperCase()) // ALMA, KÖRTE, SZILVA, BARACK
console.log(s.charAt(3)) // a
console.log(s[3]) // a

Néhány lehetőség:

  • String hossza: a str.length attribútummal kérhetjük le. Ez tehát nem metódus, és ennek oka az, hogy a string nem megváltoztatható, ezáltal a hossza sem változik.
  • String felbontása: a str.split(separator) metódussal adott karakter mentén fel tudjuk bontani a stringet. Az eredmény stringek tömbje lesz.
  • Szóközök törlése az elejéről és a végéről (beleértendő a tabulátorokat is): a str.trim() metódus hajtja végre. Az eredeti string nem változik, az eredmény egy új string lesz.
  • Keresés: a str.search(regexp) metódus visszaadja a paraméterül átadott string első előfordulásának az indexét (0-val indítva), ill. -1-et, ha nem található. Paraméterként nemcsak stringet, hanem reguláris kifejezést is kaphat. Hasonló metódus az str.indexOf(substr[, startIndex]); ennek ugyan nem adhatunk meg reguláris kifejezést, viszont második paraméterként megadhatjuk a kezdőindexet. Hasonló metódusok: str.startsWith(substr) (adott prefixszel kezdődik-e), str.endsWith(substr) (adott posztfixszel végződik-e), str.matches(regexp) adott reguláris kifejezésre illeszkedik-e.
  • Alstring: a str.substring(start, end) metódus egy string altringjét adja vissza, kezdő pozíciótól vég pozícióig (0-tól indexelve). Hasonló eljárás a slice(start, end); ez utóbbi negatív indexeket is elfogad: -1 az utolsó karaktert jelenti, -2 az utolsó előttit stb. A str.substr(start, length) második paramétere az alstring hossza.
  • Adott indexű karakter: a str.charAt(index) visszatér az adott indexű karakterrel (0-tól sorszámozva). Hasonló függvény a str.charCodeAt(index) függvény, ami a karakter UTF-16 kódjával tér vissza. Az ES5-től kezdve lehetőség van a str[index] módszerrel történő megadásra is.
  • String módosítása: minden esetben az eredeti string marad, az eredmény egy új string lesz. A str.replace(source, dest) adott stringben lecseréli egy string összes előfordulását egy másikra. A str.toUpperCase() nagybetűssé, a str.toLowerCase() kisbetűssé konvertálja.

Dátum

A dátum kezelés minden programozási nyelv fontos könyvtára, és ez alól a JavaScrit sem kivétel. A Date osztály már ősidők óta benne van a nyelvben, és magával cipel egy-két kellemetlen örökséget is. Néhány általános tudnivaló a dátumkezelésről:

  • A JavaScript is beleesett az Y2K csapdájába, ugyanis - annak ellenére, hogy lényegében a múlt évezred utolsó éveiben jelent meg - kezdetben az évet 2 számjeggyel tárolta. Így szinte az indulás pillanatában meg kellett oldani a 2000 év problémáját. A következő bölcs döntés született: a 2 jegyű (és ennél fogva az 1 jegyű) évszám jelentse az 1900-as évek adott évszámát, tehát pl. 95 az 1995-öt. A 4 jegyű pedig a tényleges évszámot, tehát pl. 2020 a 2020-at. Jótékony homályba vész a 3 jegyű évszámok problémája: a tapasztalat szerint hol abszolút évszámot jelent, hol 1900-hoz hozzá kell adni. Tehát a 120 jelentheti a második század elejét, ahogy 2020-at is.
  • Elkövették azt a tragikus döntést, hogy - a tömbök 0-tól történő sorszámozása eredményeként - a hónapokat is 0-tól sorszámozzák, tehát a 0 jelenti a januárt, a 7 az augusztust, a 11 a decembert.
  • Ugyanez a probléma jelen van a hét napjai esetén is, ugyanakkor ott van még egy csavar: a hét első (azaz nulladik) napja a zsidó naptár szerint vasárnap, és nem a keresztény naptár szerinti hétfő, így a vasárnap sorszáma a 0, a hétfőé az 1, a keddé a 2 stb.
  • Ha csak külön nem jelezzük, a megjelenítés az adott böngésző időzónájától függ, tehát máshogy fog megjelenni ugyanaz az oldal Európában mint Amerikában. Lehetőség van minden esetben UTC időt használni.
  • A JavaScript használja a Linux rendszeridőt, azaz az UTC szerinti 1970 január 1, 00:00:00 óta eltelt időt, viszont a Unixtól eltérően nem az azóta eltelt másodperceket, hanem az azóta eltelt ezredmásodperceket számolja.
  • Az ISO-8601 szabvány szerint is használhatjuk az időt.
  • Ha megpróbáljuk megjeleníteni az idő objektumot, akkor mindig a következő, hosszú formában jeleníti meg: Thu Aug 20 2020 18:22:15 GMT+0200 (Central European Summer Time).

Lássunk pár példát!

Ha a Date() osztály példányosításakor nem adunk meg paramétert, akkor az adott időpillanatot kapja értékül:

let dateNow = new Date()
console.log("Now: " + dateNow)

Megadhatjuk egyesével az idő összetevőit (év, hónap, nap, óra perc, másodperc, ezredmásodperc):

let dateYMDHMSM = new Date(2020, 7, 20, 18, 22, 15, 123)
console.log("YMDHMSM: " + dateYMDHMSM) // Thu Aug 20 2020 18:22:15 GMT+0200 (Central European Summer Time)

Figyelem: a 7 az augusztust jelenti!

Ha évként kétjegyű számot adunk meg, akkor az 1900-as évekbe kerülünk:

let dateYMDHMSMOld = new Date(95, 7, 20, 18, 22, 15, 123)
console.log("YMDHMSM old: " + dateYMDHMSMOld) // Sun Aug 20 1995 18:22:15 GMT+0200 (Central European Summer Time)

Nem kötelező mindent megadni. Ha valami kimarad, akkor annak az értéke automatikusan a legkisebb lesz:

let dateYMD = new Date(2020, 7, 20)
console.log("YMD: " + dateYMD) // Thu Aug 20 2020 00:00:00 GMT+0200 (Central European Summer Time)

Viszont legalább az évet és a hónapot meg kell adnunk. Ha csak egy paramétert adunk meg, akkor az az 1970.01.01 00:00:00 óta eltelt ezredmásodpercekként értelmezi:

let dateMillis = new Date(1597940535123)
console.log("Millis: " + dateMillis) // Thu Aug 20 2020 18:22:15 GMT+0200 (Central European Summer Time)

String formában is megadhatjuk az időpontot. Egyik lehetőség az ISO-8601 szabvány:

let dateIso = new Date("2020-08-20T18:22:15.123+02:00")
console.log("ISO: " + dateIso) // ISO: Thu Aug 20 2020 18:22:15 GMT+0200 (Central European Summer Time)

Itt eljátszhatunk az időzónával, pl.:

let dateDiffTimezone = new Date("2020-08-20T18:22:15.123-02:00")
console.log("Different timezone: " + dateDiffTimezone) // Thu Aug 20 2020 22:22:15 GMT+0200 (Central European Summer Time)

Láthatjuk, hogy átkonvertálta közép európai nyári időzónára.

Vannak még egyéb megadási módszerek is, de ezek támogatottsága már böngészőfüggő lehet:

let dateMid = new Date("Aug 20 2020")
console.log("Mid: " + dateMid)
let dateLong = new Date("August 20, 2020")
console.log("Long: " + dateLong)

A Date.parse() visszaadja a Unix ezredmásodpercet:

let msec = Date.parse("2020-08-20T18:22:15.123+02:00")
console.log(msec) // 1597940535123

Az időpont komponenseit le tudjuk kérdezni, ill. meg is tudjuk változtatni:

let dateGetterSetter = new Date("2020-08-20T18:22:15.123+02:00")
console.log("Year: " + dateGetterSetter.getYear()) // 120
console.log("Full year: " + dateGetterSetter.getFullYear()) // 2020
console.log("Hours: " + dateGetterSetter.getHours()) // 18
console.log("UTC hours: " + dateGetterSetter.getUTCHours()) // 16
console.log("Day: " + dateGetterSetter.getDay()) // 4
console.log("Old milliseconds: " + dateGetterSetter.getMilliseconds()) // 123
dateGetterSetter.setMilliseconds(456)
console.log("New milliseconds: " + dateGetterSetter.getMilliseconds()) // 456

Figyeljük meg a következőket:

  • A getYear() az 1900 óta eltelt évek számát adja vissza, míg a getFullYear() az abszolút évet.
  • A getUTCHours() UTC időpontként adja vissza az órát. Ez mindegyik komponensre működik, de az óra esetén a leglátványosabb.
  • A getDay() a nap sorszámát adja vissza. A 4 csütörtököt jelent. Emlékeztetőül: 0 a vasárnap, 1 a hétfő stb.

Számok

A számokkal kapcsolatban kezdetekkor sajnos sikerült pár nagyon átgondolatlan döntést hozni a JavaScript nyelv megalkotóinak, ami akkor jó ötletnek tűnt, de valójában olyan következményekkel járt, aminek következtében a JavaScript e tekintetben kilóg a programozási nyelvek listájából, és nagyon sok vicces mém született már belőle. Ezek a döntések a következők:

  • A JavaScript nem típusos nyelv, a változók típusát nem kell, de nem is lehet megadni. Szükség esetén a konverzió teljesen automatikusan történik, tehát pl. a 10 és a "10" többnyire egyenértékű.
  • A szám típusa kivétel nélkül mindig 64 bites lebegőpontos, egész nincsenek.
  • A + operátor egyszerre jelenti az összeadást, valamint a string konkatenálást.

A számokat megadhatjuk egészként, tizedes tört alakban, exponenciális alakban és hexadecimálisan is:

console.log(3) // 3
console.log(3.6) // 3.6
console.log(3e5) // 300000
console.log(0xFF) // 255

Egyes böngészők elfogadják az oktális alakot is, ami 0-val kezdődik.

Noha a konverzió automatikus, ezt explicit módon is kiválthatjuk (személy szerint én az explicit konverzió híve vagyok), pl.:

console.log(parseInt("3")) // 3
console.log(parseInt("3.6")) // 3
console.log(parseInt("3.6alma")) // 3
console.log(parseFloat("3.6")) // 3.6

Balról addig olvassa, amíg értelmezni tudja, a többit eldobja, szóval erre sem alapozhatunk túl fontos döntéseket.

A lebegőpontos aritmetikának meg van a maga hátránya:

console.log(0.2 + 0.1) // 0.30000000000000004

Lássunk pár példát az automatikus konverzióra:

console.log(5 == "5") // Az automatikus konverzió miatt az eredmény igaz (true).
console.log(10 + 20) // Két szám összege: 30.
console.log(10 + "20") // A + itt konkatenálást jelent. Minden stringgé konvertálódik, majd összefűzés eredménye 1020 lesz.
console.log(10 + 20 + "30") // A 10 és a 20 közötti + jel itt összeadást, a 20 és 30 közötti jel pedig összefűzést jelent. Így az eredmény 3030 lesz.
console.log("Result = " + 10 + 20) // Mivel itt már az első komponens string, az összes többi stringgé alakul, így az eredmény ez lesz: Result = 1020.
console.log("3" * "2") // A * string műveletként nem értelmezett, így számokká konvertálja a stringeket, és a műveletet végrehajtja. Az eredmény 6.
console.log(3 * "apple") //  Az eredmény számként nem értelmezhető, így a NaN (not a number) értéket kapjuk eredményül.

Ha 0-val osztunk, az eredmény nem NaN lesz, hanem végtelen:

console.log(1/0) // Infinity

A Number objektum számos hasznos konstanst definiál, pl.:

console.log(Number.POSITIVE_INFINITY) // Infinity
console.log(Number.NEGATIVE_INFINITY) // Infinity
console.log(Number.MAX_VALUE) // 1.7976931348623157e+308
console.log(Number.MIN_VALUE) // 5e-324
console.log(Number.NaN) // NaN

A számokat megadhatjuk primitívekként és objektumokként is. Az összehasonlításnál vigyáznunk kell:

console.log(new Number(5)) // Number { 5 }
console.log(5 == new Number(5)) // Az automatikus konverzió miatt igaz (true).
console.log(5 === new Number(5)) // Típusellenőrzést is végez, és mivel az 5 típusa number, a Number(5)-é pedig objekt, az eredmény hamis (false) lesz.
console.log(new Number(5) == new Number(5)) // Az objektumokat referenciaként hasonlítja össze így az eredmény hamis (false) lesz.

Nem igazán látok valós indokot arra, hogy egy számot objektumként tároljunk. Hátránya számos van:

  • Ahányszor előfordul ugyanaz a szám, annyiszor foglal helyet a memóriában.
  • A memória lefoglalása és felszabadítása lassú.
  • A kód nehezebben olvashatóvá válik.
  • Az összehasonlítás nem feltétlenül így működik, ahogy azt elvárjuk.

Előnyét viszont nem látom. Viszont ha már itt tartunk, érdemes megemlíteni a valueOf() függvényt, ami az objektumot számmá alakítja:

let xObject = new Number(5)
console.log(typeof xObject) // object
let xNumber = xObject.valueOf()
console.log(typeof xNumber) // number

Mivel a Number osztály közvetlen használata nem célszerű, elméletben a valueOf() függvénynek sem kell, hogy legyen gyakorlati jelentősége.

Noha a számok primitívek, a JavaScriot-ben bizonyos műveleteket végre tudunk hajtani rajtuk úgy, mintha objektumok lennének, pl.:

console.log((300000).toExponential(5)) // 3.00000e+5
console.log((3.6).toFixed(2)) // 3.60

A számokkal szoros kapcsolatban áll a matematika. A JavaScript tartalmaz egy Math osztályt. Ez csak statikus függvényeket ill. konstansokat tartalmaz, így nem kell példányosítani. Ízelítőül néhány lehetőség:

console.log(Math.PI) // 3.141592653589793 (a pi konstans)
console.log(Math.E) // 2.718281828459045 (az e konstans)
console.log(Math.round(3.6)) // 4 (kerekítés)
console.log(Math.ceil(3.6)) // 4 (felfele kerekítés)
console.log(Math.floor(3.6)) // 3 (lefele kerekítés)
console.log(Math.pow(2, 3)) // 8 (hatványozás)
console.log(Math.sqrt(2)) // 1.4142135623730951 (gyökvonás)
console.log(Math.abs(-3.6)) // 3.6 (abszolút érték)
console.log(Math.sin(Math.PI/4)) // 0.7071067811865476 (szinusz)
console.log(Math.min(4, 6, 7, 2, 5)) // 2
console.log(Math.max(4, 6, 7, 2, 5)) // 7

Sok más egyéb lehetőség is van: konstansok, függvények (pl. a szokásos trigonometrikus függvények); további részletekért érdemes rákeresni a lehetőségekre.

A JavaScript beépített véletlen szám generátora egy 0 és 1 közötti valós értéket ad vissza:

console.log(Math.random()) // [0..1)

Az eredmény elméletben lehet 0, de 1 soha. Ha más véletlen számra van szükségünk, pl. egy 1 és 100 közötti egészre, akkor azt magunknak kell előállítanunk, pl.:

console.log(1 + Math.floor(100 * Math.random())) // [1..100]]

Gyűjtemények

Ha logikailag összetartozó adatokat szeretnénk tárolni, akkor azok külön-külön változókban történő tárolása helyett célszerű gyűjteményt használni. A JavaScript is tartalmaz néhány gyűjteményt, melyekről itt olvashatunk részletesebben:

Tömb

Angolul array.

Létrehozás

Tömböt sokféleképpen hozhatunk létre. Az egyik lehetőség: példányosítjuk az Array osztályt, a konstruktor paramétereként megadjuk az elemszámot, és szögletes zárójelben felsoroljuk az elemeket:

let cars = new Array(3);
cars[0] = 'Trabant';
cars[1] = 'Wartburg';
cars[2] = 'BMW';
console.log(cars) // (3) ["Trabant", "Wartburg", "BMW"]

Kiíratáskor megkapjuk a tömb méretét, valamint az elemeit vesszővel felsorolva.

A konstruktorban az elemszám valójában el is hagyható; ez esetben a böngésző automatikusan lefoglal valamekkora memóriaterületet. (Ha tudjuk az elemszámot, érdemes megadnunk, mert lehet, hogy az automatikus memóriafoglalás pazarló, az átméretezés pedig drága.)

A tömböt az elemek konstruktorban történő felsorolásával is megadhatjuk:

let cars = new Array('Trabant', 'Wartburg', 'BMW');

A new kulcsszó elhagyható:

let cars = Array('Trabant', 'Wartburg', 'BMW');

Van még egy lehetőség a megadásra, az Array.of:

let cars = Array.of('Trabant', 'Wartburg', 'BMW');

Ennek különösen akkor van jelentősége, ha egy elemű, szám típusú tömböt szeretnénk létrehozni. Az Array(5) ugyanis létrehoz egy 5 elemű tömböt, az Array.of(5) pedig egy olyan tömböt, melynek egy eleme van: az 5-ös szám.

A tömb adatszerkezet annyira gyakori, hogy a nyelv megalkotói egy további egyszerűsítést hajtottak végre: szögletes zárójelben is felsorolhatjuk az elemeket, és ez esetben az Array sem kell:

let cars = ['Trabant', 'Wartburg', 'BMW'];

Hivatkozás a tömb elemeire

A továbbiakban ezt a szintaxist használjuk. Az elemekre a szögletes zárójellel [] tudunk hivatkozni:

console.log(cars[1]) // Wartburg

A hivatkozás index szerint történik. Ezzel meg is tudjuk változtatni a tömb elemeit:

cars[1] = 'Audi';
console.log(cars) // ["Trabant", "Audi", "BMW"]

Beszúrás és törlés

Beszúrni tömbbe elemet többféleképpen tudunk. A legolcsóbb a végére beszúrni ill. onnan törölni, a push() és a pop() függvényekkel (ezekkel a műveletekkel tehát veremként is használhatjuk a tömböt):

cars.push('Mercedes');
console.log(cars); // (4) ["Trabant", "Audi", "BMW", "Mercedes"]
console.log(cars.pop()); // Mercedes
console.log(cars); // (3) ["Trabant", "Audi", "BMW"]

A műveletek tehát magán a tömbön hajtódnak végre, a művelet előtti állapot elveszik.

Ennek párja a shift() ill. unshift(), ami balról szúr be ill. töröl elemet:

console.log(cars.shift()); // Trabant
console.log(cars); // (2) ["Audi", "BMW"]
cars.unshift('Opel');
console.log(cars); // (3) ["Opel", "Audi", "BMW"]

A tömb hossza

A length attribútummal kérhetjük le a tömb aktuális hosszát. Nem túl szép, de JavaScript-ben működő megoldás, ha a tömbnek még egy nem létező indexére szúrunk be elemet. Mivel a tömb indexelése 0-ról kezdődik, a length pedig a tömb méretét adja vissza, ami tehát az utolsó index + 1, a következő kódrészlet a végére szór be egy elemet:

cars[cars.length] = "Volkswagen";
console.log(cars); // (4) ["Opel", "Audi", "BMW", "Volkswagen"]

Műveletek

Elemeket rendezni a sort() művelettel tudunk, ami szintén helyben rendez:

let numbers = [2, 15, 8, 9, 4];
console.log(numbers); // (5) [2, 15, 8, 9, 4]
numbers.sort();
console.log(numbers); // (5) [15, 2, 4, 8, 9]

Hoppá, ez nem jó eredményt adott! Ábécé szerint rendezte az elemeket, noha azok számok. (Az ilyen tervezési bakik a JavaScript-ben nagyon sok vicces mém alapja.) Ha számokként szeretnénk sorba rendezni, akkor meg kell adnunk egy rendezési függvényt. Ennek két paramétere van, és pozitívat kell visszaadni, ha az első nagyobb mint a második, 0-t, ha egyenlőek, és negatívat, ha a második nagyobb mint az első. Számok esetén, nyíl függvény szemantikával ez eléggé kompakt:

numbers.sort((a, b) => a - b);
console.log(numbers); // (5) [2, 4, 8, 9, 15]

Számos más műveletet is végre tudunk rajta hajtani, melyekről a specifikációban tájékozódhatunk.

A JavaScript tömb támogatja az alapvető folyam (stream) jellegű műveleteket is. Az alábbi példák mindegyikében az eredeti tömb megmarad:

  • map(): végiglépked az elemeken, és mindegyik elemen végrehajt egy átalakítást, pl. console.log(numbers.map(x => 2 * x)) // (5) [4, 8, 16, 18, 30]
  • filter(): bizonyos elemeket kiszűr, pl. console.log(numbers.filter(x => x > 5)) // (3) [8, 9, 15]
  • forEach(): mindegyik elemen végrehajt egy műveletet, pl. a numbers.forEach(x => console.log(x)) egyesével kiírja a konzolra az elemeket.
  • reduce(): balról jobbra összevonja az elemeket, pl. az alábbi kiírja az összeget: console.log(numbers.reduce((total, x) => total + x)) // 38. Ennek párja a reduceRight, ami jobbról balra vonja össze az elemeket. Összeadásnál ugyanazt az eredményt kell kapnunk, de lehetnek olyan műveletek (pl. a kivonás), ahol már eltér a végeredmény.

Típusos tömb

A JavaScript-ben alapvetően nincsenek típusok, és ez igaz a tömbökre is. Azonban sok esetben - különösen nagyobb tömbök esetén - esetleg pazarló lehet a böngészőre bízni a reprezentációt. Például ha tudjuk, hogy a tömb elemei csak előjel nélküli 16 bites számok lehetnek, akkor pazarló lenne 64 bites lebegőpontos módszert alkalmazni, nagyobb bitszám esetén pedig pontatlanságot is eredményezhet.

let unsignedIntegers = Uint16Array.of(5, 120, 50000);

Az U előtag jelenti azt, hogy előjel nélküli. Lehetőségek: Int8Array, Uint8Array, Uint8ClampedArray (a negatív számokat 0-ra, a 255-nél nagyobb értékeket 255-re állítja, a tizedes számokat pedig kerekíti), Int16Array, Uint16Array, Int32Array, BigInt64Array, BigUint64Array, Uint32Array, Float32Array, Float64Array.

Asszociatív tömb

Angolul map. A JavaScript-ben az objektumok inkább hasonlítanak asszociatív tömbökre mint valódi, az objektumorientált világban használatos objektumokra. Így kezdetben nem is létezett ez az adattípus. Viszont idővel célszerűnek tartották a két fogalmat megkülönböztetni: az objektumot mint objektumorientált fogalmat, és az asszociatív tömböt mint adatszerkezetet. Most ez utóbbit nézzük meg.

Az asszociatív tömbök kulcs-érték párokat tartalmaznak. A Map példányosításával tudjuk létrehozni. Jellegéből fakadóan a két legfontosabb művelet egy adott kulcsú elem értékének beállítása, valamint a kulcshoz tartozó érték kiolvasása, melyet az alábbi példa illusztrál:

let myMap = new Map();
myMap.set('apple', 5);
myMap.set('banana', 2);
myMap.set('orange', 3);
console.log(myMap.get('banana'));

Természetesen számos egyéb műveletet végre tudunk hajtani, melyről a vonatkozó leírásokban és specifikációban tájékozódhatunk, pl. itt: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map.

Az asszociatív tömb egy másik megvalósítása a WeakMap. Annyi megszorítás van a Map-hez képest, hogy itt a kulcs csak objektum lehet, primitív típus nem. Ebben az a ráció, hogy ha egy kulcsra (magára az objektumra) nincs hivatkozás, akkor a garbage collector felszabadíthatja.

Halmaz

Angolul set. A halmazokban is elemeket tárolunk. Két lényeges elvi eltérés van a tömbök és a halmazok között:

  • A halmazok esetén a sorrend kevésbé értelmezett. (Az óvatos fogalmazás oka a következő: más programozási nyelvekben léteznek olyan megvalósítások, melyek megjegyzik a beszúrás sorrendjét, ill. olyanok is, amelyek esetében olyan a belső reprezentáció, hogy a bejárás sorrendje rendezett, de alapértelmezésben a halmazok esetében sem a sorrendiség, sem a rendezettség nem értelmezett. Ez azt is jelentheti, hogy két bejárás sorrendje nem feltétlenül ugyanaz. Ezzel szemben a tömbök esetén a sorrend mindig egyértelműen adott, és az elemek mindig sorba rendezhetőek.)
  • A halmazokban egy elem alapvetően egyszer fordulhat elő. (Az óvatos fogalmazás oka itt is az, hogy más programozási nyelvekben előfordulhatnak ún. multiset-ek, melyekben egy elem többször is szerepelhet. De alapvetően a halmazokban egy elem csak egyszer szerepelhet, és el eltér a tömböktől, ahol ugyanaz az elemet minden esetben többször is beszúrhatjuk.)

A halmazok esetében a két legfontosabb művelet a beszúrás, ill. annak a lekérdezése, hogy egy elem benne van-e a halmazban. Gyakori még az elemeken történő végiglépkedés:

let fruits = new Set();
fruits.add('apple');
fruits.add('banana');
fruits.add('orange');
fruits.add('apple');
console.log(fruits.size); // 3
console.log(fruits.has('apple')); // true
console.log(fruits.has('peach')); // false
for (let fruit of fruits) {
    console.log(fruit);
}

Az egyéb lehetőségekről itt is a specifikációban ill. különböző leírásokban tájékozódhatunk, pl. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set.

A WeakSet megvalósítás esetén csak objektumok lehetnek a halmaz értékei, primitívek nem.

Események

Áttekintés

Az események alapvető fontosságúak a JavaScript szempontjából, ugyanis a JavaScript műveletek tipikusan események hatására hajtódnak végre. Már a kezdeti verziókban megjelent: pl. olyasmire lehetett használni, hogy ha a felhasználó egy képként megvalósított nyomógomb fölé vitte az egérkurzort, akkor megváltoztatta a mögöttes képet, azt a hatást keltve, mintha benyomódott volna.

Rengetegféle esemény létezik, melyekről majd látunk ízelítőt; szinte nincs az az elméletben előforduló esemény, amire a JavaScript ne tudna reagálni. A gyakorlatban ez a reakció tipikusan magának a DOM-nak (azaz a kinézetnek) a megváltoztatását jelenti. De mivel ennek a szakasznak a fókusza az eseménykezelés, itt az esemény tényét kiírjuk a konzolra.

Nyomógomb

Először lássuk azt az eseményt, ami a többségnek először eszébe jut: kattintás nyomógombon:

<button onclick="console.log('My Button pressed')">My Button</button>

A fenti példából láthatjuk, hogy az esemény elé tesszük az on prefixet, és utána megadjuk azt a JavaScript kódot, amit végre szeretnénk hajtatni. Esetben esetben ez a console.log() hívást jelenti, de tipikusabb egy saját függvény meghívása.

Egérműveletek

Ha elképzeljük, hogy milyen események is létezhetnek, akkor óhatatlanul is korlátoz minket az, hogy milyen elemről van szó: egy nyomógombra kattintunk, egy szövegbeviteli mezőbe írunk, a rádiógombok közül pedig választunk. Nos, ebben az esetben elengedhetjük a fantáziánkat, ugyanis a JavaScript képes szinte minden elem esetén szinte minden eseményre reagálni. Az alábbi példában még egy konszolidált megoldást látunk, ahol egy paragrafus szöveg reagál arra, hogy a felhasználó fölé vitte az egeret (onmouseover), elhagyta azt (onmouseout), sőt, kattintani is lehet rá:

<p onmouseover="console.log('The mouse is over the text.')" 
   onmouseout="console.log('The mouse is out of the text.')"
   onclick="console.log('Clicked on the text.')">Paragraph text</p>

Egy "jól nevelt" felhasználói felületnek persze úgy célszerű működnie, ahogy a felhasználók többsége elvárja. Tehát lehetőleg csak a nyomógombok és linkek reagáljanak az egérkattintásra, a többi HTML elem ne nagyon.

Szöveg beviteli események

A szöveg beviteli mezőben a "legális" események listája is elég széles; az alábbi példában a billentyű lenyomás fázisaira (lenyomás, nyomva tartás, elengedés), vágólapról történő másolásra, valamint a fogd és vidd technikával történő másolásra látunk példát:

<input type="text" 
       onkeydown="console.log('Key ' + event.keyCode + ' is down.')"
       onkeypress="console.log('Character ' + event.charCode + ' pressed.')"
       onkeyup="console.log('Key ' + event.keyCode + ' is up.')"
       onpaste="console.log('Paste text: ' + event.clipboardData.getData('text'))"
       ondrop="console.log('Drop text: ' + event.dataTransfer.getData('text'))">

Ebben a példában megjelent az event paraméter: valójában mindegyik események van egy ilyen paramétere, melyek tulajdonságai (pl. az adatmezői) az adott esemény típusától függnek. A példában a keyCode a billentyű fizikai kódját jelenti (ez nyelvi beállítástól függetlenül ugyanaz), a charCode pedig a beírt karakter kódját. Az onpaste esemény paramétere vágólap specifikus dolgokat tartalmaz, melyek közül nyilván a legfontosabb a vágólap tartalmának a kiolvasása, a fogd és vidd technika event paramétere pedig a vágólapéra hasonlít, de nem ugyanaz.

Az alapértelmezett művelet megakadályozása

A böngészők az egyes események hatására a különböző HTML elemek esetén végrehajtják az alapértelmezett műveletet. Például ha egy szövegbeviteli mezőbe írunk szöveget, azt megjeleníti. Az event.preventDefault() hívással tudjuk ezt megakadályozni. Az alábbi példa megakadályozza az ékezetes karakterek beírását:

<input type="text" onkeypress="keyPressCheck(event)">
<script>
    function keyPressCheck(event) {
        if (event.keyCode > 127) {
            event.preventDefault();
        }
    }
</script>

További események

A fent bemutatott néhány példa csak a jéghegy csúcsa. Ahogy számos más esetben, itt is igaz az, hogy teljességre nem is törekszem. Az alábbi oldalak segítettek nekem a jobb áttekintésben:

A kategóriák az alábbiak. Mindegyik kategória számos konkrét eseményt tartalmaz, ill. a paraméterül átadott event objektum annak megfelelő mezőket.

  • AnimationEvent: CSS animációs események.
  • ClipboardEvent: vágólap események, pl. kivágás, másolás, beillesztés.
  • DragEvent: a fogd és vidd interakciót eseményei.
  • FocusEvent: fókusz események, pl. egy adott elem megkapta vagy elveszítette a fókuszt.
  • HashChangeEvent: ezek akkor következnek be, amikor megváltozik az URL-ben a # utáni rész.
  • InputEvent: akkor következik be, ha a felhasználó beír valamit egy beviteli mezőbe.
  • KeyboardEvent: billentyű interakciók.
  • MouseEvent: egér műveletek.
  • PageTransitionEvent: oldal navigációs események.
  • PopStateEvent: böngésző történet megváltozása.
  • ProgressEvent: külső erőforrás betöltése.
  • StorageEvent: a HTML5 tárolóban bekövetkező változások eseményei.
  • TouchEvent: az érintőképernyő eseményei.
  • TransitionEvent: CSS átmenetek eseményei.
  • UiEvent: a felhasználói felület eseményeinek őse. A leszármazottak: FocusEvent, InputEvent, KeyboardEvent, MouseEvent, TouchEvent, WheelEvent.
  • WheelEvent: az egér görgőjének eseményei.

A window objektum

A window objektum segítségével tud a JavaScript kommunikálni a böngészővel. A window. kiírása opcionális.

HTML elemek elérése

A JavaScript létezésének legfontosabb oka az, hogy dinamikusan meg tudja változtatni a HTML tartalmat. Ez a terület igen szerteágazó, ezért külön alfejezetben kerül leírásra. Egy gyors példa azért álljon itt is, ami előrevetíti a lehetőségeket és a módszer erejét:

<div id="demo"></div>
<script>
window.document.getElementById('demo').innerHTML = 'Hello, world!';
</script>

Felugró ablakok

A böngészőben felugró ablakokban lehet információt megjeleníteni, ill. kérdést feltenni.

Figyelmeztetés

Az alert() segítségével tudunk figyelmeztető üzenetet írni egy felugró ablakban:

window.alert('Hello, world!')

Jóváhagyás

Jóváhagyás-elutasítás felugró kérdést a confirm() segítségével tudunk létrehozni:

if (window.confirm('Do you confirm?')) {
    console.log('yes')
} else {
    console.log('no')
}

Kérdés

Kérdést a prompt() hívással tudunk feltenni:

let name = window.prompt('Name:', 'Csaba')
console.log('Hello, ' + name + '!')

Fontos azért tudnunk, hogy a felhasználók (mi magunk is) nem igazán kedvelik azokat az oldalakt, amelyek feldobnak ablakokat, azokat ösztönösen kerülik, szóval tényleg nagyon módjával használjuk ezeket a lehetőségeket. Érdemes más módszert alkalmazni, pl. a figyelmeztetést a szövegtől eltérő módon, célszerűen piros, vastag betűkkel ráírni magára az oldalra, az eldöntendő kérdést jelölőnégyzet (checkbox) segítségével feltenni, míg kitöltendő kérdést a szövegbeviteli mező segítségével. Ezek leginkább csak tanuláshoz és a dolgok kipróbálásához alkalmasak. Pl. ha ki szeretnénk íratni valamit, de nem szeretnénk létrehozni a HTML oldalt, ahova be tudjuk írni, és a konzol logot is túl rejtettnek találjuk.

Méretek

Az alábbi méretek lekérdezésére van szükség:

  • screen.height, screen.width: a képernyő felbontása.
  • screen.availHeight, screen.availWidth: a rendelkezésre álló képernyő felbontása (tipikusan a Windows feladat tálca (taskbar) nélküli képernyő mérete).
  • outerHeight, outerWidth: a böngésző ablak mérete.
  • innerHeight, innerWidth: az ablak tartalmának méretei (beleértve a görgetősávot is). Ezt angolul viewport-nak hívjuk.
  • pageXOffset (= scrollX), pageYOffset (= scrollY): a görgetett képpontok száma.
  • screen.colorDepth, screen.pixelDepth: színmélység bitben (az operációs rendszeré ill. a monitoré; manapság mindkettő 24).

Az alábbi példa kiírja a teljes képernyő felbontást, valamint a böngésző ablak felbontását:

console.log('Screen resolution: ' + window.screen.width + ' x ' + window.screen.height);
console.log('Viewport resolution: ' + window.innerWidth + ' x ' + window.innerHeight);

Ablak műveletek

Az open() függvény segítségével új ablakot nyithatunk, míg a close()-zal bezárhatjuk. Számos paramétert megadhatunk, melyre itt nem térünk ki; most csak egy egyszerű példát láthatunk a használatára. Az open.html tartalma legyen a következő:

<button onclick="window.open('close.html')">Open new page</button>

A close.html-é pedig ez:

<button onclick="window.close()">Close this page</button>

Nyissuk meg a böngészőből az open.html-t, kattintsunk a nyomógombra, majd a felugró (vagy másik fülön megjelenő) ablakban kattintsunk az ottani nyomógombra.

Fontos tudnunk, hogy a felhasználók nem szeretik, ha egy ablak nyitása ill. bezárása automatikusan történik, így ezeket a függvényeket lehetőleg ne használjuk.

Adott oldal adatai

A location objektum tartalmazza az adott oldal elérési útvonalát. Azon belül:

  • href: a teljes elérési útvonala, pl. http://127.0.0.1:5500/index.html.
  • protocol: a protokol, ami vagy http:, vagy https:.
  • hostname: a szerver neve, pl. localhost.
  • port: a port száma, ami általában 80 vagy 443; a fenti példában a Visual Studio Code alapértelmezett 5500-as számú portja látható.
  • pathname: az elérési útvonal szerveren belül, pl. /index.html.

Példa a kiíratásra:

console.log('URL: ' + window.location.href)
console.log('protocol: ' + window.location.protocol)
console.log('hostname: ' + window.location.hostname)
console.log('port: ' + window.location.port)
console.log('pathname: ' + window.location.pathname)

Az assign() metódussal át tudjuk irányítani az oldalt egy másikra:

window.location.assign('http://faragocsaba.hu/javascript')

Előzmények

A window.history függvényei segítségével tudunk programozottan navigálni a böngésző történetben:

  • window.history.back(): visszalépés eggyel.
  • window.history.go(n): előre lépés n-nel. Tehát pl. go(-1) ekvivalens azzal, hogy back().

Az alábbi példát úgy érdemes kipróbálni, hogy előtte megnyitunk ugyanabban az ablakban egyéb oldalakat is:

<html>
    <body>
        <button onclick = "window.history.back()">Go back</button>
        <button onclick = "window.history.go(-2)">Go back 2</button>
    </body>
</html>

Sütik

A sütik (angolul cookie) kulcs-érték párok, amelyek a webes lekérdezés metainformációjaként az adat kommunikáció részei. A böngészőben tárolódnak, és adott szerverre vonatkoznak. Ha legközelebb egy kérés megy egy adott szerverhez, akkor a hozzá tartozó süti automatikusan része lesz a kérésnek. A szerver ki tudja olvasni az adatokat belőle, és módosítani is tudja. Kiolvasni és módosítani kliens oldalon, a böngészőben is tudjuk. Itt azt nézzük meg, hogy ezt hogyan tudjuk megtenni JavaScript segítségével.

Megjegyzés: a sütik tipikus használata szerver oldali. Mivel nem lehet garantálni, hogy egy munkamenet során mindig ugyanaz a szerver szolgáltja ki a kliens kéréseit, a HTTP - természetéből fakadóan - állapotmentes. Ez az egyetlen módszer arra, hogy a szerver képes legyen összekapcsolni a kéréseket korábbiakkal. Sütik nélkül pl. nem lehetne webshopban vásárolni, ami tipikusan több lépésből áll, és a szervernek tudnia kell azt, hogy az adott lépés mely korábbi lépéseknek a következője. A sütik kliens oldali, tehát böngészőből történő állítása a gyakorlatban atipikus.

Tároló

A HTML5-ben bevezettek egy böngészőben eltárolt kis adatbázist, melyben pár megabájt adatot tudunk tárolni. A sütikhez hasonlóan itt is kulcs-érték párokban tárolhatjuk az adatokat, az viszont lokális. A tárolót két részre oszthatjuk:

  • localStorage: ennek nincs lejárata. Elméletileg sohasem törlődik, csak ha explicit töröljük.
  • sessionStorage: adott munkamenetre vonatkozik, hosszú távon automatikusan törlődik.

Az alábbi példa a két legfontosabb műveletet: a beállítást és a kiolvasást mutatja be:

window.localStorage.setItem("key", 25)
console.log(window.localStorage.getItem("key"))
 
window.sessionStorage.setItem("sessionKey", 50)
console.log(window.sessionStorage.getItem("sessionKey"))

További műveletek:

  • length: a kulcs-érték párok száma.
  • key(n): visszaadja az n-edig kulcs nevét.
  • removeItem(key): törli az adott kulcsú elemet.
  • clear(): törli az összes adatot.

Földrajzi pozíció

Egyre több eszköz képes kisebb-nagyobb pontossággal megállapítani a felhasználó földrajzi pozícióját, és egyre több webalkalmazás használja ezt ki. A pozíció megállapításának egyik, de nem egyetlen módszere a műholdas helymeghatározás (pl. a GPS), melyet elsősorban navigációra használhatunk, de pl. egy jól irányzott reklámhoz ennél kevésbé pontos helymeghatározás is elegendő; pl. egy pizzéria reklámhoz elég azt tudnia, hogy épp melyik városrészben tartózkodunk.

A JavaScript-ben a navigator objektumon keresztül tudjuk a felhasználó pozícióját lekérdezni, pl.:

window.navigator.geolocation.getCurrentPosition(position => {
    console.log(position.coords.latitude + ", " + position.coords.longitude)
})

Ha megnyitjuk az oldalt, akkor engedélyt kér a földrajzi pozíció meghatározásához. Ha megadjuk, akkor a konzolon megjelenik a kisebb.nagyobb pontosságú földrajzi pozíciónk.

Ez persze csak a jéghegy csúcsa. Egyrészt a szélességi és hosszúsági koordinátákon kívül még sok más egyebet is le tudunk kérdezni (tengerszint feletti magasság, pontosság, magasság pontosság, irány, sebesség stb.), be tudjuk kapcsolni a folyamatos figyelést, ill. meg tudjuk térképen jeleníteni. Ez utóbbihoz megfelelő előfizetésre van szükségünk.

Időzítés

JavaScript-ben időzíteni a setTimeout() függvénnyel tudunk, melynek két paramétere van:

  • a függvény, amit végre szeretnénk hajtani az időzítés leteltével,
  • az időzítés hossza ezredmásodpercben.

Pl.:

console.log("Before set timeout")
window.setTimeout(() => console.log("Timeout finished"), 1000)
console.log("After set timeout")

A Timeout finished kb. egy másodperccel az After set timeout után íródik ki.

Törölni az időzítést annak lejárta előtt a window.clearTimeout() függvénnyel tudjuk (a window objektumról még lesz szó bővebben):

console.log("Before set timeout")
let myTimeout = window.setTimeout(() => console.log("Timeout finished"), 1000)
console.log("After set timeout")
window.clearTimeout(myTimeout)
console.log("Timeout cleared")

Intervallum

Ha nem egyszer, hanem többször (potenciálisan végtelen sokszor) szeretnénk időzíteni (pl. órát készíteni), akkor jobb megoldás az intervallum használata:

console.log("Before set interval")
window.setInterval(() => console.log("Tick"), 1000)
console.log("After set interval")

Az After set interval után másodpercenként kiírja a konzolra a Tick üzenetet.

Intervallumot törölni a window.clearInterval() függvénnyel lehet:

let myInteral = window.setInterval(() => console.log("Tick"), 1000)
window.setTimeout(() => window.clearInterval(myInteral), 10500)

DOM műveletek

A DOM a Document Object Model rövidítése, ami a dokumentum felépítését takarja. Mivel a DOM átalakítása a JavaScript központi lényege, az évtizedek során rendkívül sok lehetőség fejlődött ki, melyek részletes bemutatása nem célja, nem is lehet célja ennek a leírásnak. Néhány lehetőséget fogunk megnézni, melyek segítségével elképzelésünk lesz ennek a lehetőségnek az erejéről.

A lehetőségek áttekintése

A műveleteket a window.document (vagy csak document) objektum segítségével tudjuk végrehajtani. Önkényesen a következő kategóriákba sorolom a lehetőségeket:

  • Lekérdezések:
    • getElementById(): azonosító (id) alapján, pl. <div id="demo"> és window.document.getElementById('demo').
    • getElementsByTagName(): lekérdezés a HTML tag neve alapján. Mivel egy dokumentumon belül tetszőleges számú adott tag lehet (pl. akárhány kép, azaz img tag lehet), a visszatérés nem egy adott HTML elem, hanem HTML elemek gyűjteménye.
    • getElementsByClassName(): adott osztályú (azaz class attribútummal rendelkező) elemek lekérdezése. Itt is egy lekérdezés eredménye nem egy elem, hanem gyűjtemény. Ezzel elsősorban a dokumentum kinézetét lehet beállítani, mivel a class tipikusan CSS elemekre hivatkozik.
  • Bejárások:
    • childNodes: adott HTML elem gyermek elemei.
    • parentNode: szülő elem.
    • firstChild: első gyermek elem.
    • lastChild: utolsó gyermek elem.
    • nextSibling: következő testvér.
    • previousSibling: előző testvér.
  • Tartalomkezelés
    • innerHTML: az adott elem belső HTML tartalmának a lekérdezés ill. beállítása.
    • createElement(): HTML elem létrehozása.
    • appendChild(): HTML elem hozzáadása egy másik alá.
    • replaceChild(): egy HTML elem lecserélése egy másikra.
    • removeChild(): egy HTML elem törlése.
    • write(): közvetlen írás a HTML dokumentumba.
  • Eseménykezelés
    • on…(): tetszőleges HTML elem után írva létrehozhatjuk ill. felülírhatjuk bármely eseménykezelést, pl. getElementById('myButton').onclick(…).

Az alábbiakban némely lehetőségre láthatunk példát. Az alábbi HTML dokumentummal fogunk dolgozni:

<!DOCTYPE html>
<html>
    <head>
        <title>Test document</title>
    </head>
    <body>
        <div id="demo"></div>
        <p class="red">banana</p>
        <p class="green">orange</p>
        <p class="blue">apple</p>
        <p class="green">cherry</p>
        <p class="red">peach</p>
        <p class="blue">lemon</p>
        <script src="dom.js"></script>
    </body>
</html>

Elemek lekérdezése ID alapján

A fenti HTML oldalban van egy ilyen rész:

<div id="demo"></div>

Az alábbi kódrészlet (dom.js) beleírja azt, hogy Hello, world!.

window.document.getElementById('demo').innerHTML = 'Hello, world!';

Ha tehát mindent jól csináltunk, akkor megjelenik a Hello, world! felirat! Nézzünk be egy kicsit a "motorháztető alá"! Először nézzük meg a böngészőben az oldal forrását (pl. Ctrl + U)! A lényegi rész ez:

<div id="demo"></div>

Ez tehát nem változott. Most nézzük meg a HTML elemeket a fejlesztőeszközökön (Developer Tools) belül: tipikusan F12, majd Elements fül. Ott már ezt látjuk:

<div id="demo">
    "Hello, world!"
</div>

Tehát a JavaScript dinamikusan megváltoztatta a HTML kódot, ami azonnal meg is jelent a böngésző ablakba! Nem kellett újra tölteni az oldalt, az újratördelés a dokumentum megváltozásával automatikusan, azonnal megtörtént.

Ha megértettük a fentieket, akkor most kell, hogy legyen egy "aha" élmény: mekkora hatalmas lehetőség rejlik a JavaScript-ben! Nem véletlen tehát, hogy a nyelv népszerűsége rekordokat döntöget!

Elemek lekérdezése tag alapján

A getElementsByTagName() az összes ugyanolyan HTML tag-et kérdezi le. Mivel tetszőleges számú lehet, az eredmény gyűjtemény lesz, melyen végig lehet lépkedni:

let paragraphs = window.document.getElementsByTagName('p');
for (const paragraph of paragraphs) {
    console.log(paragraph.innerHTML);
}

Egyúttal példát láthatunk a HTML tartalom lekérdezésére is.

Más elemek lekérdezése

Nincs az a HTML elem, amit ne lehetne lekérdezni. A https://www.w3schools.com/js/js_htmldom_document.asp felsorolja a lehetőségeket. Ott találunk olyanokat is, amelyeket másképp nem tudnánk lekérdezni (pl. a document.title a <head><title>…</title></head> részben megadott cím), de szép számmal vannak olyanok is, amelyeket megfelelő lekérdezéssel más módon is megkaphatnánk, de egyszerűsíti a fejlesztő dolgát (pl. a document.images az összes képet azaz az <img> tag-ű elemet adja vissza).

Az alábbi példa kiírja a konzolra a dokumentum címét:

console.log(window.document.title);

Kinézet megváltoztatása

Az alábbi példában az azonos osztályú (class) elemeket kérdezzük le, és beállítjuk az írás színét. Az írás színét CSS segítségével tudjuk beállítani, mégpedig a style attribútumot kell valamilyen értékre beállítani. A végeredmények kb. a következőnek kell lennie:

<p style="color: red">red text</p>

A setAttribute() hívással be tudjuk állítani tetszőleges HTML elem tetszőleges attribútumát, ami szintén azonnal megjelenik az oldalon. Például az alábbi kód az összes class="red" elemet pirosra állít:

for (const redElement of window.document.getElementsByClassName('red')) {
    redElement.setAttribute('style', 'color: red');
}

Egy másik talán kézenfekvőbb megoldás az, hogy nem a setAttribute() függvényt hívjuk, hanem az adott elemnek property-ként beállítjuk a stílusát, a következőképpen:

for (const redElement of window.document.getElementsByClassName('green')) {
    redElement.style = 'color: green';
}

Ezzel valójában kihasználtuk, hogy a JavaScript egy script nyelv (azért a Java nyelv karbantartói bajban lennének, ha igény lenne egy ilyen egyszerűsítésre).Ez tetszőleges attribútum esetén működik. A példában ezzel "festettük" zöldre a megfelelő gyümölcsneveket.

A stílus kellően fontos attribútum ahhoz, hogy a JavaScript ebben a speciális esetben még egy lehetőséget biztosít, amit a "kékfestésnél" használunk ki:

for (const redElement of window.document.getElementsByClassName('blue')) {
    redElement.style.color = 'blue';
}

Az az attribútum, amit be szeretnénk állítani, jelen esetben a szín (color), nem stringként, hanem attribútumként jelenik meg.

HTML elem létrehozása

Az innerHtml beállításával valójában bármit létre tudunk hozni, mégis, HTML kódok stringben történő létrehozása helyett elegánsabb a HTML tartalmat fokozatosan felépíteni. Az alábbi példa a createElement() segítségével létrehoz egy <p> tag-et, annak beállítja a belső HTML-ét (ami már egy szöveg, további HTML tag-eket az egyszerűség érdekében nem tartalmaz), majd az appendChild() segítségével a demo elem alá fűzzük:

let generatedParagraph = window.document.createElement('p');
generatedParagraph.innerHTML = 'Generated paragraph text.';
window.document.getElementById('demo').appendChild(generatedParagraph);

Ha megnézzük a HTML elemeket, az alábbit fogjuk látni:

<div id="demo">
    "Hello, world!"
    <p>"Generated paragraph text."</p>
</div>

Ebben benne van a korábban beállított Hello, world! is. Láthatjuk, hogy nem lecserélte a HTML tartalmat, hanem valóban hozzáfűzte.

Események megváltoztatása

Az alábbi példa az esemény megváltoztatását mutatja be:

window.document.getElementById('demo').onmouseover = function() {console.log('Mouse over demo.')}

És itt is érdemes kihangsúlyozni, hogy tetszőleges HTML elem tetszőleges eseményét felüldefiniálhatjuk; nem kell annak az események "jól fésültnek" lennie. Bár túlzásba semmiképpen se essünk: a felhasználó ugyanis értelemszerűen a nyomógombra kattintani, a szövegbeviteli mezőbe pedig írni szeretne, és nem fordítva.

Írás a dokumentumba

A document.write() mintegy "beleírja" a dokumentumba a paraméterül átadott értéket, mégpedig pont oda, ahol fut a JavaScript. Például a következő sor kód a pillanatnyi időpontot írja bele:

window.document.write(Date());

Még akkor is, ha ez a JavaScript fájl első sora, ha a HTML-ből a hivatkozás a végére került, akkor a végén lesz ez is.

Űrlapkezelés

A web hajnalán az űrlap volt a kevés interakciós lehetőségek egyike, és ennek mind a mai napig kitüntetett szerepe van. Kezdetben nem volt lehetőség kliens oldali ellenőrzésre: ahogy a felhasználó beírta az adatokat, azt úgy küldte el a böngésző a szervernek. Ha hibásan töltötte ki, akkor újra be kellett tölteni a teljes oldalt, és a nulláról ismét kitölteni. Talán nem is érdemes szót fecsérelni arra, hogy miért kiáltott ez már kezdettől fogva kliens oldali ellenőrzésért, és az űrt természetesen a JavaScript töltötte ki. Azóta persze sok minden történt; ebben a szakaszban néhány lehetőséget nézünk meg, messze nem a teljesség igényével.

Mindegyik esetben a formanyomtatvány egyetlen beviteli mezőből fog állni, ahova a felhasználónak egy 1 és 10 közötti értéket kell beírnia. JavaScript nélküli HTML-ben ez valahogy így nézne ki:

<html>
    <body>
        <p>Enter an integer between 1 and 10</p>
        <form name="myform" action="send.php">
            <input type="text" id="mynum">
            <input type="submit" value="Submit">
        </form>
    </body>
</html>

Most persze tekintsünk el attól, hogy a mögötte a backend nem létezik, és koncentráljunk arra, hogy kihozzuk a maximumot böngészőn belül.

Űrlap elemeinek ellenőrzése

Az első ellenőrzési lehetőség valahogy így néz ki:

<html>
    <body>
        <p>Enter an integer between 1 and 10</p>
        <form name="myform" action="send.php" onsubmit="return myValidate()">
            <input type="text" id="mynum">
            <input type="submit" value="Submit">
        </form>
        <p id="message"></p>
        <script>
function myValidate() { 
    const inputValue = document.forms['myform']['mynum'].value;
    let resultText = '';
    let result = false;
    if (isNaN(inputValue)) {
        resultText = 'The input value is not a number.';
    } else if (inputValue < 1) {
        resultText = 'The input value is less than 1.';
    } else if (inputValue > 10) {
        resultText = 'The input value is greater than 10.';
    } else if (Math.round(inputValue) != inputValue) {
        resultText = 'The input value is not integer.';
    } else {
        result = true;
    }
    document.getElementById('message').innerHTML = resultText;
    return result;
}            
        </script>
    </body>
</html>

Tehát a <form> tag-nek van egy onsubmit attribútuma, ami egy logikai értékkel tér vissza, ami ha igaz, akkor az űrlap elküldhető, egyébként nem. A példában a myValidate() függvényt hívja, ami lekérdezi a myform űrlap mynum elemének az értékét, és ha hibát talál, akkor kijelzi, egyébként tovább engedi.

Nagyobb kontroll

A JavaScript-ben a fentinél nagyobb kontroll is elérhető, és valójában nincs is szükség a <form> tag-re:

<html>
    <body>
        <p>Enter an integer between 1 and 10</p>
        <input type="text" id="mynum">
        <button onclick="myValidate()">Check</button>
        <p id="message"></p>
        <script>
function myValidate() {
    const inputValue = document.getElementById('mynum').value;
    let resultText = '';
    if (isNaN(inputValue)) {
        resultText = 'The input value is not a number.';
    } else if (inputValue < 1) {
        resultText = 'The input value is less than 1.';
    } else if (inputValue > 10) {
        resultText = 'The input value is greater than 10.';
    } else if (Math.round(inputValue) != inputValue) {
        resultText = 'The input value is not integer.';
    } else {
        resultText = 'The input value is OK: ' + inputValue;
    }
    document.getElementById('message').innerHTML = resultText;
}            
        </script>
    </body>
</html>

A validációs rész a fentihez hasonló, itt viszont a nyomógombra helyeztük azt.

Ellenőrzés gépelés közben

A fenti megoldásnak már meg van az az előnye, hogy feleslegesen nem fog elmenni az űrlap a szerverhez, viszont a rendszer még mindig megengedi, hogy rosszul töltse ki a felhasználó, és csak a végén írjon ki akár több tucat hibaüzenetet. Jó lenne, ha már gépelés közben kapnánk visszajelzést. A JavaScript-ben ez is megoldható, figyelők létrehozásával:

<html>
    <body>
        <p>Enter an integer between 1 and 10</p>
        <input type="text" id="mynum">
        <p id="message"></p>
        <script>
 
const inputField = document.getElementById('mynum');
inputField.addEventListener('input', function(event) {
    const inputValue = document.getElementById('mynum').value;
    let resultText = '';
    if (isNaN(inputValue)) {
        resultText = 'The input value is not a number.';
    } else if (inputValue < 1) {
        resultText = 'The input value is less than 1.';
    } else if (inputValue > 10) {
        resultText = 'The input value is greater than 10.';
    } else if (Math.round(inputValue) != inputValue) {
        resultText = 'The input value is not integer.';
    } else {
        resultText = 'The input value is OK: ' + inputValue;
    }
    document.getElementById('message').innerHTML = resultText;
})
        </script>
    </body>
</html>

A példában a mynum beviteli mezőhöz rendeltünk esemény figyelőt, az 'input' jelenti azt, hogy már a bevitel (gépelés, beillesztés vágólapról, húzd és vidd) során hajtsa végre az ellenőrzést. Ha tehát nem megfelelő értéket írunk be, akkor azt azonnal jelzi. Ennek különösen akkor van jelentősége, ha az űrlap sok elemből áll.

HTML szintű ellenőrzés

Az űrlapba beírt adatok ellenőrzése annyira alapvető, hogy a HTML szabvány is nyújt rá megoldást, az 5-ös verziótól kezdve (bár számomra nem az a különös, hogy most már ad, hanem az, hogy korábban nem adott). Ismét egy olyan példát látunk, melyben nincs JavaScript:

<html>
    <body>
        <p>Enter an integer between 1 and 10</p>
        <form name="myform" action="send.php">
            <input type="number" id="mynum" min="1" max="10" required>
            <input type="submit" value="Submit">
        </form>
    </body>
</html>

Ez sajnos nincs annyira tökélyre fejlesztve, mint amit JavaScript-tel el lehet érni, de egyszerűbb esetekben valójában bőven megteszi.

Form API

Ha ki szeretnénk használni a HTML5 ellenőrzési lehetőségeit és a JavaScript rugalmasságát egyszerre, akkor a Form API-t tudjuk használni. Ez is egy, a példában bemutatottnál jóval szerteágazóbb példa, itt JavaScript-ben hajtatjuk végre a HTML5 szintű ellenőrzést, a hibaüzenetet viszont nem hagyjuk kiírni az alapértelmezett helyre, hanem a szokásos helyre íratjuk:

<html>
    <body>
        <p>Enter an integer between 1 and 10</p>
        <input type="number" id="mynum" min="1" max="10" required>
        <button onclick="myValidate()">Submit</button>
        <p id="message"></p>
        <script>
function myValidate() { 
    const inputElement = document.getElementById('mynum');
    let resultText = '';
    if (!inputElement.checkValidity()) {
        resultText = inputElement.validationMessage;
    } else {
        resultText = 'The input value is OK: ' + inputElement.value;
    }
    document.getElementById('message').innerHTML = resultText;
}            
        </script>
    </body>
</html>

Ízelítőnek talán ennyi elegendő. Számomra a https://www.w3schools.com/js/js_validation.asp és a https://www.w3schools.com/js/js_validation_api.asp oldalak segítettek a téma megértésében, ezeket ajánlom a további részletek megismerésére is.

Aszinkronitás

Előfordulhat, hogy egy műveletet nem azonnal szeretnénk végrehajtani, hanem egy bizonyos idő elteltével. Pl. adott ideig tartó inaktivitás automatikusan léptesse ki a bejelentkezett felhasználót. Ennek egyfajta párja az, amikor egy művelet hosszú ideig tart, és nem szeretnénk blokkolni a folyamatot. Ezekről a témákról lesz szó ebben a szakaszban.

Promise

Ha van egy bizonytalan ideig tartó, hosszú művelet (pl. letöltés a netről), és nem szeretnénk blokkolni a futást, akkor a "gyalogos" megoldás az, hogy rendszeresen figyeljük, befejeződött-e már a művelet. A következő példában azt szimuláljuk, hogy az eredmény 5 és fél másodperc múlva érkezik meg, és másodpercenként ránézünk, hogy megjött-e már:

let result = undefined
setTimeout(() => result = "apple", 5500)
 
function pollResult() {
    if (result) {
        console.log("Result: " + result)
    } else {
        console.log("Waiting for the result…")
        setTimeout(pollResult, 1000)
    }
}
 
pollResult()

Itt a lassúságot a fent megismert setTimeout() segítségével szimuláltuk. (Direkt "alvás", pl. delay() vagy sleep() nincs a JavaScript-ben.)

Ennél elegánsabb megoldás a Promise osztály használata. A szintaxisát lássuk egy példán keresztül:

let myPromise = new Promise(function(resolve, reject) {
    setTimeout(() => resolve("apple"), 1000)
})
 
console.log("Waiting for result...")
myPromise.then(
    result => console.log(result),
    error => console.log(error)
)

A Promise osztály konstruktora egy olyan függvényt vár paraméterül, melynek két paramétere van, az is két függvény: resolve és reject. A két paraméter függvényről nem kell gondoskodnunk, hanem meg kell hívnunk: a resolve()-ot akkor, ha sikeres a művelet, a reject()-et pedig akkor, ha nem.

A kliens oldalon leggyakrabban a .then módszerrel tudjuk kezelni a választ. Ennek két ún. callback (visszahívandó) függvényt kell megadni. A rendszer az elsőt akkor hívja, ha sikerült a művelet, a másodikat akkor, ha nem. A fenti példa a sikeres esetet mutatja, és a konzolon egy másodperc elteltével megjelenik az apple szó.

Nem kötelező a második paraméter; lehetőség van csak a sikeres esetre felkészülni:

myPromise.then(
    result => console.log(result)
)

Lássunk egy példát a negatív esetre:

let myPromise = new Promise(function(resolve, reject) {
    setTimeout(() => reject("test error message"), 1000)
})
 
console.log("Waiting for result...")
myPromise.then(
    result => console.log(result),
    error => console.log(error)
)

Ha csak a hibás esetre vagyunk kíváncsiak, akkor a then() helyett használjuk a catch()-et:

myPromise.catch(console.log)

Ha ugyanúgy szeretnénk kezelni a normál lefutást és a hibát is, akkor azt a finally() hívással tehetjük meg:

myPromise.finally(() => console.log("Finished"))

Ez ekvivalens azzal, mintha a then()-nek kétszer adtuk volna meg ugyanazt.

Egyszerre több is megadható, lényegében akármennyi:

myPromise
    .finally(() => console.log("Finished"))
    .then(
        result => console.log(result),
        error => console.log(error)
    )

Ebben a példában függetlenül attól, hogy sikerült-e a művelet vagy sem, először kiírja, hogy Finished, majd a sikerességétől függően folytatódik a then()-en belül a futás.

Paramétert úgy tudunk átadni, hogy a Promise-t függvényen belülre tesszük. Az alábbi példában a setTimeout() segítségével azt szimuláljuk, hogy van egy lassan működő összeadó:

let longAdd = function (a, b) {
    return new Promise(function(resolve, reject) {
        setTimeout(() => resolve(a + b), 1000)
    })
}
 
console.log("Waiting for result...")
longAdd(3, 2).then(
    result => console.log(result)
)

Az alábbi példa a hosszú idek tartó osztásra ad példát. Itt a rendes és a hibás eset is le van kezelve; ez utóbbi a nullával történő osztás esetén:

let longDivide = function (a, b) {
    return new Promise(function(resolve, reject) {
        setTimeout(() => {
            if (b != 0) {
                resolve(a / b)
            } else {
                reject("Nullával osztás")
            }
        }, 1000)
    })
}
 
console.log("Waiting for result...")
 
function callLongDivide(a, b) {
    longDivide(a, b).then(
        result => console.log(result),
        error => console.log(error)
    )
}
 
callLongDivide(6, 3)
callLongDivide(6, 0)

async … await

A fenti szintaxis a visszaívó (callback) megoldás miatt kicsit "nyakatekert". Létezik erre egy "egyenesebb" szintaxis, az alábbi módon:

function lateFruit() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve("apple")
        }, 1000)
    })
}
 
async function f() {
    const res = await lateFruit()
    console.log(res)
}
 
console.log("Before f() call")
f()
console.log("After f() call")

A példában a lateFruit() egy Promise osztályt ad vissza, ami egy másodperc elteltével adja az eredményt. Az egyik lehetőség ennek kezelésére a fent bemutatott .then megoldás lenne. A másik az, amit a fenti példában tettünk: létre hoztunk egy aszinkron függvényt (f()), és ott az await kulcsszóval adtuk meg azt, hogy várja meg a lateFruit() eredményét, majd írja ki. Mindez aszinkron módon zajlik, tehát először kiíródik a Before f() call, majd az After f() call, végül egy másodperc elteltével az apple.

Reflection

A reflection nem JavaScript specifikus fogalom. Általában azt jelenti, hogy kicsit "megerőszakoljuk" a nyelvet oly módon, hogy nyelvi struktúrákat nem úgy hozunk létre ill. hívunk meg, ahogy az a nagykönyvben meg van írva, hanem megfelelő hívások segítségével. Igen ritkán fordul elő az, hogy tényleg ilyenre kelljen "vetemednünk", célszerű kerülni, ha csak lehet.

A JavaScript-ben ezek a függvények a Reflect osztályon belül találhatóak. Talán így egy kicsit homályos, de példákon keresztül érteni fogjuk, hogy miről van szó.

Nekem a téma feldolgozását a https://www.javascripttutorial.net/es6/javascript-reflection/ leírás segítette.

Függvényhívás

Példaként tekintsük a következő Math.max() függvényhívást:

console.log(Math.max(3, 2, 5, 8));

Reflection segítségével ugyanez a következő:

console.log(Reflect.apply(Math.max, Math, [3, 2, 5, 8]));

A fenti példa a Math osztály metódusa, melyet második paraméterként kellett átadni. Ha a függvényünk nem osztályon belül szerepel, akkor a módszer az alábbi:

function add(a, b) {
    return a + b;
}
 
console.log(Reflect.apply(add, undefined, [3, 2]));

Objektum létrehozása

Az alábbi módszerrel tudunk objektumot példányosítani reflection segítségével:

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
 
    fullName() {
        return this.firstName + " " + this.lastName
    }
}
 
let person = Reflect.construct(Person, ['John', 'Smith']);
console.log(person.fullName());

Objektum kiterjesztése

Az előző példát folytatva, a következőképpen adunk hozzá újabb attribútumot:

Reflect.defineProperty(person, 'age', {
    writable: true,
    configurable: true,
    enumerable: false,
    value: 40,
})
 
console.log(person.age);

JSON

A JSON a JavaScript Object Notation rövidítése. Eredetileg tehát a JavaScript objektumok szerializálására találták ki, de a formátum olyan jól sikerült, hogy a JavaScripttől független formátummá nőtte ki magát, úgymond önálló életre kelt, és az írás pillanatában ez a legelterjedtebb kódolási formátum. Főbb jellemzői:

  • Formátuma ember és gép által is jól olvasható szöveg.
  • Szintaxisa hasonlít ahhoz, ahogy a JavaScript objektumokat meg kell adni.
  • Az egész egy nyitó kapcsos zárójellel kezdődik, és záró kapcsos zárójellel végződik ({…}).
  • Kulcs-érték párokat tartalmaz. A kulcs mindenképpen string, melyeket kötelezően dupla idézőjelek közé kell tenni. A kulcsot és az értéket a kettőspont választja el. Az érték lehet:
    • string (szintén kötelezően dupla idézőjelek között),
    • másik objektum ({…}),
    • tömb (szögletes zártójelben, vesszővel felsorolva: ([…, …, …])).
  • A JavaScript objektumtól a következőkben tér el:
    • A kulcs is string (a JavaScript objektum esetén az mezőnév, melyet nem kell idézőjelek közé tenni).
    • A JSON csak a kétszeres idézőjelet fogadja el (a JavaScript string lehet egyszeres és kétszeres is).
    • A JSON nem tartalmaz függvényeket (azok a JavaScript objektumok, melyek tartalmaznak függvényt, konverziókor a függvényt törli).
    • a JSOn nem ismeri a dátum típust, azt automatikusan stringgé konvertálja (ennek a visszakonvertáláskor van jelentősége).

A JSON formátumot a legtöbb programozási nyelv kisebb-nagyobb mértékben támogatja. Általában külső könyvtárra van ehhez szükség. A JavaScript beépítve tartalmazza mindkét irányú konvertálást:

  • JSON.stringify(object): ezzel konvertáljuk az objektumot JSON formátummá.
  • JSON.parse(str): ezzel konvertáljuk vissza objektummá.

Lássunk egy példát!

let person = {firstName: "Csaba", lastName: "Faragó", age: 43}
let personJson = JSON.stringify(person)
console.log(personJson) // {"firstName":"Csaba","lastName":"Faragó","age":43}
let personParsed = JSON.parse(personJson)
console.log(personParsed) // Object { firstName: "Csaba", lastName: "Faragó", age: 43 }
console.log(personParsed.firstName) // Csaba

Ha belegondolunk, hogy az objektumok szerializálása és deszerializálása milyen hatalmas utat tett meg az elmúlt évtizedek során, akkor döbbenünk csak meg igazán azon, hogy mennyire egyszerű ez a JavaScript-ben. Valójában elég csak a fenti két, valóban egyszerű függvényt megjegyeznünk.

Érdemesnek tartom megjegyezni a JSON lehetséges alternatíváit.

Amikor még minden bit számított, akkor a kódolás binárisan történt, A fenti JSON kódolt adat ({"firstName":"Csaba","lastName":"Faragó","age":43}) ASN.1 BER enkódolással a következőképpen néz ki:

30138005 43736162 61810746 61726167 C3B38201 2B

Egy másik lehetséges választás az XML:

<?xml version="1.0" encoding="UTF-8"?>
<Person>
  <firstName>Csaba</firstName>
  <lastName>Faragó</lastName>
  <age>43</age>
</Person>

Azt gondolom, hogy olvashatóság és tömörség tekintetében a legoptimálisabb választás egyértelműen a JSON.

Két apróság maradt igazából ki: a függvények és a dátumok szerializálása.

A függvények szerializálása természetellenes, azt nem szokás szerializálni. Gondoljunk csak bele: ha a JSON nyelvfüggetlen formátumként tartalmaz JavaScript függvényeket, az más rendszereken jó eséllyel nem fog futni. "Végszükség" esetén viszont erre is van mód: stringgé kell alakítani a függvényt, majd visszaalakítás után az eval() függvény segítségével tudjuk futtatni:

let person = {
    firstName: "Csaba",
    lastName: "Faragó",
    age: 43,
    fullName: function() {return this.firstName + " " + this.lastName}
}
let personJson = JSON.stringify(person)
console.log(personJson) // {"firstName":"Csaba","lastName":"Faragó","age":43}
person.fullNameStr = person.fullName.toString()
let personJsonFunc = JSON.stringify(person)
console.log(personJsonFunc) // {"firstName":"Csaba","lastName":"Faragó","age":43,"fullNameStr":"function() {return this.firstName + \" \" + this.lastName}"}
let personJsonFuncParsed = JSON.parse(personJsonFunc)
person.fullName = eval("(" + person.fullNameStr + ")")
console.log(person.fullName()) // Csaba Faragó

A dátumok szerializálása, ill. annak hiánya már annál érthetetlenebb! Ha megpróbáljuk szerializálni, majd deszerializálni, akkor az eredmény string lesz:

let person = {
    firstName: "Csaba",
    lastName: "Faragó",
    age: 43,
    fullName: function() {return this.firstName + " " + this.lastName}
}
let personJson = JSON.stringify(person)
let person = {firstName: "Csaba", lastName: "Faragó", birthDate: new Date(1977, 6, 7)}
console.log(person) // Object { firstName: "Csaba", lastName: "Faragó", birthDate: Date Thu Jul 07 1977 00:00:00 GMT+0100 (közép-európai téli idő) }
let personJson = JSON.stringify(person)
let personParsed = JSON.parse(personJson)
console.log(personParsed) // Object { firstName: "Csaba", lastName: "Faragó", birthDate: "1977-07-06T23:00:00.000Z" }

Mondjuk legalább az ISO 8601 formátumban tárolja, ami a számításba jövő lehetőségek közül a legjobb (bár figyeljük meg, hogy a közép-európai téli idő miatt egy órával, így egy nappal korábbra állította), de még így is végső soron string. Ahhoz, hogy dátumot kapjunk, magunknak kell gondoskodnunk a string dátummá alakításáról. Ezt kétféleképpen tehetjük meg:

  • Utólag egyszerűen módosítjuk a megfelelő mezőt.
  • Már átalakításkor módosítjuk, mégpedig úgy, hogy átadunk a JSON.parse() függvénynek egy második paramétert, ami az ún. reviver függvény.

Ez utóbbira lássunk egy példát!

let person = {
    firstName: "Csaba",
    lastName: "Faragó",
    age: 43,
    fullName: function() {return this.firstName + " " + this.lastName}
}
let personJson = JSON.stringify(person)
let personParsedReviver = JSON.parse(personJson, function(key, value) {
    if (key == "birthDate") {
        return new Date(value)
    } else {
        return value
    }
})
console.log(personParsedReviver) // Object { firstName: "Csaba", lastName: "Faragó", birthDate: Date Thu Jul 07 1977 00:00:00 GMT+0100 (közép-európai téli idő) }

AJAX

Áttekintés

Az AJAX az Asynchronous Javascript And XML rövidítése. Ez önmagában nem egy interfész, hanem egy megoldása annak a feladatnak, melyben a böngésző adatot kérdez a szervertől miután betöltődött az oldal, és az oldal újratöltése nélkül megjeleníti azt.

A művelet elvileg lehet akár szinkron is, akkor viszont megakad a program futása. Az aszinkron tehát nem szükséges, hanem célszerű, ugyanis abban az esetben a felhasználó a betöltés során is folyamatosan használhatja az oldalt, így jobb a felhasználói élmény.

Az XML arra utal, hogy kezdetben ez volt az adatátviteli formátum. Az alapértelmezett formátum nyilván a HTML, ehhez képest jelentett újat az XML: az adat úgymond nyers formában érkezett, és a formázás a JavaScript feladata volt. Az írás pillanatában az XML mint adatátviteli formátum a böngésző és a szerver között visszaszorulóban van, helyét átvette a JSON. Valószínűleg a történelmi hűség érdekében maradt meg mégis az AJAX elnevezés.

Kezdetben böngésző specifikus volt az AJAX megvalósítás, ma már a szabványos és széles körűen támogatott XMLHttpRequest osztályt tudjuk erre a célra használni. Valamint természetesen más egyéb megoldásokat is, pl. Fetch API, amelyek túllépnek a kezdeti megoldásokon.

Az alábbi példákat webszerver segítségével tudjuk végrehajtani. Számos webszervert használhatunk. Tesztelési célra talán a legcélszerűbb megoldás a XAMPP, ami magába foglalja a webes fejlesztéshez szükséges legfőbb komponenseket: Apache webszerver PHP-vel, MySQL, Tomcat, és még pár komponens. A https://www.apachefriends.org/hu/download.html oldalrül tölthetjük le. Telepítés után indítsuk el a XAMPP vezérlőpultját, és ott indítsuk el az Apache programot. A htdocs könyvtárba helyezzük a fájlokat (pl. nálam ez a c:\programs\xampp\htdocs\ könyvtár). Pl. ha ott létrehozunk egy test.html fájlt tetszőleges (lehetőleg szabályos HTML) tartalommal, és megnyitjuk a http://localhost/test.html oldalt, akkor webszerveren keresztül nyitottuk meg az oldalt.

Szövegfájl dinamikus betöltése

Az alábbi példában ha a felhasználó rákattint a nyomógombra, akkor dinamikusan betölti egy fájlt tartalmát, és megjeleníti azt. A megjelenítendő fájl (ajax.txt) tartalmaz az alábbi:

Hello, AJAX world!

A HTML oldal (ajax.html):

<html>
    <body>
        <div id="ajaxtext">Initial text</div>
        <button onclick="changeText()">Change text</button>
        <script src="ajax.js"></script>
    </body>
</html>

A JavaScript (ajax.js) pedig a lényeget tartalmazza:

function changeText() {
    let request = new XMLHttpRequest();
    request.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            document.getElementById('ajaxtext').innerHTML = this.responseText
        }
    }
    request.open('GET', 'ajax.txt', true);
    request.send();
}

Másoljuk be a fájlokat a fent említett htdocs könyvtárba (bár ez a példa egyébként még megy a Visual Studio Code beépülő webszerverével is), majd nyissuk meg a http://localhost/ajax/ajax.html oldalt. Kattintsunk a nyomógombra. Újratöltés nélkül olvasta be és jelenítette meg a böngésző az új tartalmat. (Ekkor a tartalommal ez persze nem nyilvánvaló, különösen nem lokális szerveren; érdemes eljátszani nagy méretű képekkel, esetleg távoli szerverrel.)

A HTML talán nem szorul magyarázatra. A JavaScript kód magyarázata:

  • A XMLHttpRequest osztály az első szabványosított és széles körben támogatott AJAX műveleteket megvalósító osztály.
  • Az onreadystatechange egy visszahívó (callback) függvény, ami állapotváltozáskor hívódik meg. A folyamatnak 5 állapota van, 0-tól 4-is sorszámozva; a 4-es az, hogy betöltődött a kért adat.
  • A document.getElementById() rész módosítja a HTML oldalt. Erről már volt szó részletesen.
  • A status a HTML státuszra utal.
  • Az open() függvény első paramétere a módszer (pl. GET, POST stb.), a második a cél, a harmadikkal pedig azt jelezzük, hogy aszinkron legyen a hívás. A false jelenti a szinkront; ez esetben nem is kellene az onreadystatechange, viszont blokkolódna a hívás.
  • A send() meg csak úgy ott van. (Ha én valósítottam volna meg, akkor elég lenne az open().)

Dinamikus tartalom betöltése

A felni példa, habár jól mutatja a lehetőségeket, mégsem annyira izgalmas, az AJAX-szal betöltött tartalom ugyanis statikus. Lássunk egy másik példát, melyet PHP-ban készítünk el! Itt egy beviteli mezőbe kell beírni egy női nevet. Amint leütöttünk egy billentyűt, a program feldobja az összes, adott betűvel kezdődő lehetőséget, majd tovább gépelve értelemszerűen szűkíti azt. (Hasonló ez a Google keresőben megszokotthoz, hogy gépelés során is megpróbálja kiegészíteni; amiatt nem azt valósítjuk meg, mert alapból nem támogatja HTML, trükközni kell a CSS-sel. Az meg már nem annyira elegáns, hogy érdemes legyen oktató anyagba beletenni.)

A HTML oldal (ajaxphp.html) a következő:

<html>
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <p>Írj be egy női nevet:</p>
        <input type="text" onkeyup="showHint(this.value)">
        <p id = "txtHint"></p>
        <script src="ajaxphp.js"></script>
    </body>
</html>

A fejlécben levő karakterkódolás itt amiatt van, hogy a magyar karakterek rendesen megjelenjenek.

A JavaScript kód (ajaxphp.js) talán nem okoz túl nagy meglepetést:

function showHint(str) {
    let request = new XMLHttpRequest();
    request.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            document.getElementById('txtHint').innerHTML = this.responseText
        }
    }
    request.open('GET', 'gethint.php?q=' + str, true);
    request.send();
}

A gethint.php tartalma az alábbi:

<?php
header('Content-Type: text/html; charset=ISO-8859-2');
 
$names = file("http://www.nytud.mta.hu/oszt/nyelvmuvelo/utonevek/osszesnoi.txt", FILE_IGNORE_NEW_LINES);
$q = $_REQUEST["q"];
$hint = "";
 
if ($q !== "") {
    $q = strtolower($q);
    $qLength = strlen($q);
    $namesLength = count($names);
    for ($i = 1; $i < $namesLength; $i++) {
        $name = $names[$i];
        if (stristr($q, substr($name, 0, $qLength))) {
            if ($hint === "") {
                $hint = $name;
            } else {
                $hint .= ", $name";
           }
        }
    }
}
 
echo $hint;
?>

Ha véletlenül nem elérhető a megadott link, akkor lokálisan használhatjuk, mely innen letölthető: osszesnoi.txt. Ha véletlenül ez sem elérhető; fájl felépítése a következő:

Az MTA Nyelvtudományi Intézete által anyakönyvi bejegyzésre alkalmasnak minősített utónevek jegyzéke: női nevek -- 2020. szeptember 1.
Abélia
Abiáta
Abigél
Ada
Adala
Adalberta
Adalbertina
Adalind
Adaora
Adél
Adela
Adéla
Adelaida
Adelgund
Adelgunda
Adelheid
...

Ebben a programban is oda kell figyelni a karakterkódolás beállítását. A példa valószínűleg nem optimális; pl. a neveket az első letöltés után valószínűleg célszerű lenne lementeni és lokálisan olvasni, vagy inkább memóriában tárolni. A PHP kód optimalizálása messze túlmutat ennek a témának a keretein.

Aszinkron JavaScript XML-lel

Végül álljon itt egy példa, ami talán a legközelebb áll az eredetileg megálmodott AJAX megoldáshoz (az aktuális JavaScript szintaxist használva). Ez a következőket tartalmazza:

  • Az oldal betöltését követően JavaScript segítségével betöltünk egy XML dokumentumot, ami az országok listáját tartalmazza a fővárosaival. Dinamikusan legenerálunk egy táblázatot, melyben az ebben a dokumentumban kapott adatokat jelenítjük meg.
  • A fenti táblázatot úgy valósítjuk meg, hogy ha a felhasználó az ország nevére kattint, akkor dinamikusan betölti az adott országra vonatkozó további adatokat, jelen esetben a legnagyobb városait, lakosságszámmal együtt, és azt felsorolásként jeleníti meg, dinamikusan.
  • A példa bemutatja az XMLHttpRequest osztályba beépített XML kezelő működését is.

Ezzel az alábbiakat érjük el:

  • Az oldal dinamikusan tölti be az adatokat.
  • Az adatok valóban XML formátumúak.
  • A böngésző nem tölti be újra a teljes oldalt.
  • A böngésző csak a szükséges adatmennyiséget tölti be.

A gyakorlatban a dinamikus tartalom is többnyire dinamikusan generálódik, pl. adatbázis lekérdezés hatására, és többnyire nem XML formátumú, hanem JSON. A példában az XML-eket fixen létrehozzuk.

countries.xml:

<?xml version="1.0" encoding="UTF-8"?>
<countries>
    <country>
        <id>hu</id>
        <name>Magyarország</name>
        <capital>Budapest</capital>
    </country>
    <country>
        <id>de</id>
        <name>Németország</name>
        <capital>Berlin</capital>
    </country>
    <country>
        <id>it</id>
        <name>Olaszország</name>
        <capital>Róma</capital>
    </country>
</countries>

hu.xml:

<?xml version="1.0" encoding="UTF-8"?>
<cities>
    <city>
        <name>Budapest</name>
        <population>1729</population>
    </city>
    <city>
        <name>Debrecen</name>
        <population>211</population>
    </city>
    <city>
        <name>Szeged</name>
        <population>168</population>
    </city>
    <city>
        <name>Miskolc</name>
        <population>167</population>
    </city>
    <city>
        <name>Pécs</name>
        <population>156</population>
    </city>
</cities>

de.xml:

<?xml version="1.0" encoding="UTF-8"?>
<cities>
    <city>
        <name>Berlin</name>
        <population>3520</population>
    </city>
    <city>
        <name>Hamburg</name>
        <population>1787</population>
    </city>
    <city>
        <name>München</name>
        <population>1450</population>
    </city>
    <city>
        <name>Köln</name>
        <population>1060</population>
    </city>
    <city>
        <name>Frankfurt</name>
        <population>732</population>
    </city>
</cities>

it.xml:

<?xml version="1.0" encoding="UTF-8"?>
<cities>
    <city>
        <name>Róma</name>
        <population>2617</population>
    </city>
    <city>
        <name>Milánó</name>
        <population>1242</population>
    </city>
    <city>
        <name>Nápoly</name>
        <population>962</population>
    </city>
    <city>
        <name>Torinó</name>
        <population>872</population>
    </city>
    <city>
        <name>Palermo</name>
        <population>657</population>
    </city>
</cities>

A HTML kód az alábbi (countries.html):

<html>
    <head>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <table id="countryTable"></table>
        <div id="cities"></div>
        <script src="countries.js"></script>
    </body>
</html>

A példában használunk CSS stílust is, mivel az alapértelmezett HTML táblázat nem tartalmaz vonalakat (style.css):

table,th,td {
    border : 1px solid black;
    border-collapse: collapse;
    padding: 5px;
}

Végül a példa lényege: a JavaScript kód (countries.js):

loadCountries();
 
function loadCountries() {
    let request = new XMLHttpRequest();
    request.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            populateCountryTable(this);
        }
    };
    request.open('GET', 'countrylist.xml', true);
    request.send();
}
 
function populateCountryTable(countries) {
    let countryCells='<tr><th>Ország</th><th>Főváros</th></tr>';
    let countryList = countries.responseXML.getElementsByTagName('country');
    for (let i = 0; i < countryList.length; i++) { 
        let countryId = countryList[i].getElementsByTagName('id')[0].childNodes[0].nodeValue;
        let countryName = countryList[i].getElementsByTagName('name')[0].childNodes[0].nodeValue;
        let countryCapital = countryList[i].getElementsByTagName('capital')[0].childNodes[0].nodeValue;
        countryCells += `
            <tr>
                <td onclick="loadCities('${countryId}', '${countryName}')"><u>${countryName}</u></td>
                <td>${countryCapital}</td>
            </tr>
        `;
    }
    document.getElementById('countryTable').innerHTML = countryCells;
}
 
function loadCities(countryId, countryName) {
    let request = new XMLHttpRequest();
    request.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            populateCityTable(this, countryName);
        }
    };
    request.open('GET', `${countryId}.xml`, true);
    request.send();
}
 
function populateCityTable(cities, countryName) {
    let citiesHtml = `
        <p>${countryName} legnagyobb városai (lakosság szám, ezer fő):
        <ul>
    `;
    let cityList = cities.responseXML.getElementsByTagName('city');
    for (let i = 0; i < cityList.length; i++) { 
        let cityName = cityList[i].getElementsByTagName('name')[0].childNodes[0].nodeValue;
        let cityPopulation = cityList[i].getElementsByTagName('population')[0].childNodes[0].nodeValue;
        citiesHtml += `<li>${cityName} (${cityPopulation})</li>`;
    }
    citiesHtml += '</ul></p>';
    document.getElementById('cities').innerHTML = citiesHtml;
}

A példában az újdonság a getElementsByTagName() hívás, mellyel ki tudjuk nyerni az XML-ből az információt. Kicsit nyakatekert módon történik, de legalább működik.

Web API

A JavaScript tartalmaz még számos könyvtárat, amivel igen szerteágazó feladatokat tudunk végrehajtani. Ezek bemutatása messze meghaladja ennek az oldalnak a kereteit. Az érdeklődők itt böngészhetik a lehetőségeket: https://developer.mozilla.org/en-US/docs/Web/API. A web API legmélyebb bugyraiban találunk böngészőspecifikus osztályokat is.

Ezek közül itt egyet mutatok be, ez a Fetch API. Ez a XMLHttpRequest modernebb továbbgondolása, ami az ES6-os újításokat is tartalmazza. A példában a HTML és a PHP kód ugyanaz, mint az AJAX-os PHP-s példában láthattuk, a JavaScript-et kell csak átírni, mégpedig a következőre:

function showHint(str) {
    fetch('gethint.php?q=' + str)
        .then(response => response.text())
        .then(hint => document.getElementById('txtHint').innerHTML = hint)
}

Jó lenne az oktatóanyagot itt befejezni, és azt mondani, hogy ez sokkal egyszerűbb és tömörebb mint az XMLHttpRequest, a probléma viszont ezzel az, hogy az ékezetes karakterekkel rosszul működik. Hiába, a régi technológiáknak meg van az az előnyük, hogy érettek, az újak pedig lehetnek fejlettek, de ott ha elhagyjuk a szűkebb értelemben vett komfortzónát, akkor sokkal több problémába botlunk.

A megoldás az alábbi, bár így már nem is igazán tömörebb, különösen nem áttekintetőbb:

function showHint(str) {
    fetch('gethint.php?q=' + str)
        .then(response => response.arrayBuffer())
        .then(buffer => {
            let decoder = new TextDecoder("iso-8859-2");
            let hint = decoder.decode(buffer);
            document.getElementById('txtHint').innerHTML = hint;
        })
}
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License