JavaScript

Kategória: Web fejlesztés.

javascript.jpg
Table of Contents

Áttekintés

A JavaScript története

Története az 1990-es évek elejéig nyúlik vissza. Akkoriban a weboldalak - mai szemmel nézve - igen egyszerűek voltak: szöveget, képeket, linkeket és beviteli mezőket tartalmaztak csak. Interakciót a linkeken kívül az űrlapok jelentették: be lehetett írni valamit, rá kellett nyomni a megfelelő nyomógombra, a kérés elment a szerver felé, majd a válasz során teljesen újratöltődött a lap. A betárcsázós internet korában, a kezdeti böngészőkkel ez úgy nézett ki, hogy a nyomógomb megnyomásakor fehér lett a képernyő, és még szerencsés esetben is jó pár másodperc eltelt, mire megjelent valami.

Az internet terjedésével megjelent az igény a jelentősebb felhasználói élményt biztosító weboldalak iránt. Tehát olyan lehetőségekre, hogy a weboldal tartalma megváltozzon anélkül, hogy az teljesen újratöltődne. Kezdetben apróságokra kell gondolni: pl. arra, hogy egy kép, ami nyomógombként is funkcionál, megváltozzon, ha a felhasználó az egérrel a kép fölé megy. A trükk az volt, hogy valójában két kép szerepelt, és "meg volt mondva", hogy mikor melyik képet kell megjeleníteni.

A nagy kérdés az, hogy hogyan volt "megmondva". Erre a HTML szabvány nem biztosított semmit. A komolyabban vehető böngészők adtak saját megoldásokat. Ennek nyilvánvaló hátránya az volt, hogy csak abban a böngészőben működött. A Netscape úttörő volt ebben a folyamatban, megalkotta a Mocha nevű script nyelvet, amiből később LiveScript lett, végül ebből lett a JavaScript. Ezt szabványosították végül ECMAScript néven. A JavaScript szabványokat tehát az ECMA tartja karban, és így is hivatkozunk rá, pl. ES5 (az ECMAScript 5-ös verziója) vagy ES6 (a 6-os verzió).

Térjünk vissza a '90-es évekre! A Microsoft is hamarosan előállt a saját megoldásával az Internet Explorerben, JScript néven. Ez nem teljesen felelt meg a JavaScript-nek. És a gyakorlatban pont úgy nézett ki, mint ahogy elképzeljük: a mindenhol (és ez a mindenhol akkoriban a Netscape-et és az Internet Explorert jelentette) működő interaktív weboldalak forrása tele volt tűzdelve olyanokkal, hogy "ha Netscape, akkor ez, ha Internet Explorer, akkor az" (esetleg: ha lynx, akkor meg amaz stb.).

A '90-es évek végére a Netscape igazságtalan versenyhátrányba került az Internet Explorerrel szemben, mivel ez utóbbit a Microsoft a Windows operációs rendszerekhez alapértelmezésben feltelepítette. De azért a Netscape is sok döntésével házhoz ment a pofonért. Miután a Netscape bedobta a törülközőt, a sírján főnixmadárként feltámadó böngészők alapból ezt támogatták.

A többi böngésző - különösen az Internet Explorer - csak nagyon lassan, és - finoman fogalmazva - sajátosan kezdte el alkalmazni a szabványt. Magyarán az Internet Explorerrel semmi sem úgy működött, ahogy kellett volna. A 2000-es évek elejére az a faramuci helyzet állt elő, hogy az Internet Explorer gyakorlatilag egyeduralkodóvá vált, az volt "a" böngésző (2002-ben a piaci részesedése 95% felett volt), és volt egy, böngészőkre vonatkozó szabvány, amit pont a piacot döntőrészt uraló böngésző nem támogatott teljes egészében. Majd megjelentek az olyan, ma is ismert böngészők, mint a Firefox, az Opera vagy a Safari, majd pár évvel később a Google Chrome (ez utóbbi a jQuery megjelenése után), és ezek fokozatosan teret hódítottak maguknak, nagyon lassan, de biztosan kiszorítva az Internet Explorert.

Amint azt ebből is láthatjuk, a script nyelvek bevezetését nem előzte meg komoly tervezés, eléggé ad hoc módon történt. Volt egy probléma, arra adtak egy megoldást, és azzal nem igazán törődtek, hogy milyen hosszú távú mellékhatást vezetnek be kitörölhetetlenül. A kezdeti átgondolatlanság a mai napig érezteti hatását. Nagyon sok JavaScript mém született, amelyek a nyelv negatív tulajdonságait figurázzák ki; az oldal tetején egy ilyen gyöngyszemet láthatunk.

A JavaScript szerepe

A JavaScript - ahogy a nevéből is következik - egy script nyelv. Viszont - a nevével ellentétben - semmi köze sincs a Java-hoz. Alapvetően a böngészőben fut, bár létezik egyéb felhasználási területe is, pl. a NodeJS szerver oldali JavaScript.

A JavaScript alapvető fontosságú a webes fejlesztésben, a tartalom formáját leíró HTML és annak formázását leíró CSS mellett. Ez tehát a böngészőben fut. Alkalmas bármilyen logika megvalósítására, valamint arra is, hogy megváltoztassa a weboldal tartalmát anélkül, hogy újra kelljen tölteni azt. Mivel több milliárd weboldal létezik, melyek jó részében van JavaScript, és több milliárd böngésző van feltelepítve, a szabványon változtatni gyakorlatilag lehetetlen, annak csak a lassú bővítése lehetséges.

Az írás pillanatában az egyik legkeresettebb programozási nyelv.

A JavaScript helye

Mivel a JavaScript a böngészőben fut, nem kell semmit sem telepíteni a használatához. (A böngészőt meglétét adottságnak feltételezem, már csak amiatt is, mert ezt a szöveget valamilyen böngészőn keresztül lehet olvasni.) A kódot valami módon be kell ágyazni a HTML-be. Erre látunk most néhány módszert.

A forrást elvileg bármilyen szövegszerkesztővel elkészíthetjük; én a Visual Studio Code-ot használtam a példák elkészítéséhez. Elvileg elég a böngészőből megnyitni a html oldalt a példák kipróbálásához; én mégis a Live Server nevű beépülő segítségével webszerverként indítom. Ennek előnye az, hogy ha megváltozik a tartalom, azonnal frissül.

HTML attribútumok

JavaScript kódot betehetünk közvetlenül valamelyik HTML tag attribútumaként, értékül. Pl.:

<!DOCTYPE html>
<html>
    <head>
        <title>Test</title>
    </head>
    <body>
        <button onclick="alert('Hello, world!')">Click me</button>
    </body>
</html>

Ebben az esetben a nyomógomb (<button>) elem kattintás (onclick) attribútumához rendeltünk egy JavaScript utasítást, ami egy felugró ablakban kiírja, hogy "Hello, world!". Ez lehetett a JavaScript első felhasználási módja.

A <script> tag

Egy másik lehetőség - különösen, ha egy utasításnál hosszabb a kód - <script> tag-ek közé helyezni. Ez elvileg bárhol elhelyezkedhet; a gyakorlatban célszerű vagy a fejlécbe (head) tenni, vagy a törzs (body) legaljára. Egy példát láthatunk a fejlécbe helyezésre:

<!DOCTYPE html>
<html>
    <head>
        <title>Test</title>
        <script>
            alert('Hello, world!')
        </script>
    </head>
    <body>
    </body>
</html>

Amikor a HTML szabványba beletették a <script> tag-et, akkor meg kellett mondanunk, hogy milyen scriptet helyezünk oda, pl. <script type="text/javascript">. Időközben hivatalosan is a JavaScript a HTML alapértelmezett script nyelve lett, így nem kötelező megadni a type attribútumot.

Külső fájl

A leggyakoribb megoldás az, hogy a HTML és a JavaScript fizikailag is elkülönül; a JavaScript kód külön forrásfájlba kerül. A JavaScript szokásos kiterjesztése .js. Ez esetben hivatkozunk kell a HTML kódból a JavaScript-re a <script> tag src attribútuma segítségével, pl.:

<!DOCTYPE html>
<html>
    <head>
        <title>Test</title>
    </head>
    <body>
        <script src="hello.js"></script>
    </body>
</html>

A példában a body-ba került a script. A hello.js-re történik hivatkozás, ami a html fájl mellett található, a következő tartalommal:

alert('Hello, world!')

Kombinált előfordulás

Egy weboldalon több helyen is lehet JavaScript kód, és ezek tudnak egymásról. Az alábbi példában a fejlécben létrehozunk egy függványt, amit egy HTML elem attribútumában hívunk meg:

<!DOCTYPE html>
<html>
    <head>
        <title>Test</title>
        <script>
            function showHelloWorld() {
                alert('Hello, world!')
            }
        </script>
    </head>
    <body>
        <button onclick="showHelloWorld()">Click me</button>
    </body>
</html>

Konzol log

A programozás oktatásban alapvető fontosságú a konzol, ahova az eredményeket írjuk, ill. ellenőrizni tudjuk, hogy valami tényleg úgy működik, ahogy le van írva. A legtöbb esetben eleve konzolból indítjuk az alkalmazást, és a program oda írja az eredményt. Mivel a JavaScript böngészőben fut, a helyzet itt egy kicsit trükkösebb. Persze mivel a JavaScript képes módosítani a HTML tartalmat (emiatt találták ki), oda is írhatjuk, csakhogy a HTML tartalom módosítása már egy egy haladóbb téma, nem ezzel kezdjük. Viszont a JavaScript-nek is van konzolja, és ez kissé "el van rejtve" a böngészőben. A konzol log elérése a legtöbb böngészőből (pl. Google Chrome és Firefox esetében biztos): F12, és a megjelenő Developer Tools eszközben többnyire a második fül.

Konzolra a console.log('…') utasítással tudunk írni, pl.:

<!DOCTYPE html>
<html>
    <head>
        <title>Test</title>
        <script>
            console.log('Hello, world!')
        </script>
    </head>
    <body>
    </body>
</html>

Ha megnyitjuk az oldalt, nem látunk semmit. De a F12 → Console fülön ott található a kiírt szöveg.

Kapcsolata a Java-val

Adódik a kérdés, hogy van-e a JavaScript-nek bármi kapcsolata a Java-val; egészen pontosan adódik a feltételezés, hogy igen. Erre a válasz csípőből az, hogy dehogy, nem, egyáltalán, a JavaScriptnek semmi köze sincs a Java-hoz, hiszen az előbbi egy böngészőben futó, interpretált script nyelv, a Java pedig egy bájtkódra fordított objektumorientált nyelv. Kismillió keltérés van: pl. a Java típusos, a JavaScript nem stb. stb. stb. A Java fejlesztők kicsit talán le is nézik a JavaScript fejlesztőket, gondolván, hogy a Java az egy komoly programozási nyelv, a JavaScript meg - hát - nem az.

A fentiek részben igazak is, de azért érdemes a képet árnyalni. Létezik a pszichológiában egy olyan jelenség, hogy ha nagyon sokat mondogatunk valakinek valamit, akkor előbb-utóbb olyanná válik; emiatt érdemes a gyereket lehetőleg minél többször megdicsérni, és kerülni az olyan kijelentéseket, hogy "buta vagy". Lehet, hogy a programozásban is ez hat; a több évtizedes megnevezés, amiben benne van a Java, oda hatott, hogy a JavaScript elkezdett sokban hasonlítani a Java-ra. Pl. a feltételkezelési struktúrák, ciklusok, kivételkezelés, osztályok szintaxisa stb. nagyon hasonlít, vagy teljesen ugyanaz, mint a Java esetében. Különösen a 2015-ben megjelent ES6 szabványban jelentek meg olyan dolgok, amelyek olyan közel vitték szintaxisban a JavaScript-et a Java-hoz, hogy már nem lehet elintézni azzal a félmondattal, hogy "semmi köze sincs hozzá". Valójában semmi köze sincs hozzá, ez igaz, de ez a mondat túl sommás, és ma már nem fejti ki az igazság minden apró részletét.

Tananyagok

Igen sok weboldal foglalkozik a JavaScript-tel, mivel az írás pillanatában ez az egyik legnépszerűbb programozási nyelv. Ezek közül az alábbiakat emelem ki:

Nyelvi elemek

Szintaxis

A JavaScript legfontosabb alaptulajdonságai:

  • Nem típusos nyelv, a változók típusát nem kell megadni.
  • A típuskonverzió automatikus.
  • Az utasításokat pontosvesszővel (;) zárhatjuk le, de nem kötelező a használata.
  • Főprogram külön nincs; a végrehajtó fentről lefelé sorban végrehajtja az utasításokat.
  • A szintaxis sok esetben nagyon hasonlít a Java-hoz.
  • Egy soros megjegyzést a //, több sorosat pedig a /* … */ struktúrával tudunk létrehozni.
  • A blokkokat kapcsos zárójelek közé tesszük: {…}.
  • A műveletek, az értékadás és az összehasonlítás a legtöbb nyelvben megszokott szintaxissal történik.
  • A feltételkezeléssel és ciklusokkal kapcsolatos struktúrák szintaxisa szintén a Javaban megszokott: if … else, switch … case … default, for, while stb.
  • Ugró (goto) utasítás itt sincs, viszont break és continue van.
  • Függvényeket a function kulcsszóval tudunk létrehozni. A paraméterek típusát és a visszatérési érték típusát nem kell megadni, egyebekben megegyezik a szintaxis a Java-ban megszokottal (pl. function add(a, b) {return a + b}).
  • Létezik kivételkezelés, szintén a Java-hoz hasonló szintaxissal: try … catch … finally, ill. van hozzá tartozó throw utasítás. De itt nincs szigorú kivétel hierarchia, valamint nem kell (nem is lehet) megadni azt, hogy melyik függvény milyen kivételt vált ki.
  • A JavaScript funkcionális nyelv is, és lambda kifejezéseket is használhatunk. Ezeket nyíl függvényeknek (arrow function) hívjuk, pl. (a, b) => a + b.
  • Objektumorientált lehetőségeket is tartalmaz.
  • Alapból a legfontosabb alap adatszerkezet elérhető: lista, halmaz, asszociatív tömb, számos hasznos művelettel.
  • Gazdag a matematika, a string és a dátum kezelés könyvtára is.
  • Tetszőleges HTML elemet képes létrehozni, megváltoztatni, törölni, számos keresési lehetőséggel.

Változók

A JavaScript nem típusos nyelv: a változóknak nem kell (és nem is tudjuk) megadni a típusát.

Változók megadása

A változókat deklarálni kell a használat előtt, például az alábbi módon:

var a = 2
console.log(a)

A változóknak alapvetően kétféle megadási módjuk van:

  • A fenti példában is látható var kulcsszóval. Ez volt a kezdeti megadási módszer, de mára idejétmúlttá vált, használatát célszerű kerülni.
  • A 2015-ben megjelent ES6-os szabványban bevezették a let és a const megadási módokat, a változók és a konstansok számára. A const kulcsszóval megadott "változó" értéke később nem módosítható. Az új megadási mód bevezetésének az oka kettős: egyrészt megjelent a blokk hatókör (scope), másrészt a funkcionális programozás; mindkettőről lesz szó a későbbiekben.
let b = 3
b = 4
const c = 5;
c = 6 // hiba

Megjegyzés: a var változók újra definiálhatóak. Az alábbi tehát helyes:

var x = 5
console.log(x)
var x = "alma"
console.log(x)

Ez ellent mond az elvárásainknak, ill. más programozási nyelvekben megszokottakkal. A let esetében az újradefiniálás hibát ír ki.

Típusok és műveletek

A típusokat tehát nem adjuk meg, de ettől függetlenül a változóknak vannak típusai. Az alábbi típusokat különböztetjük meg:

  • Number (szám): ez alatt tetszőleges számot értünk, tehát egészet, tizedes törtet, negatívat, pozitívat. A szokásos szám megadási módszerek (pl. 123, -14.2, 5e5, 0xFA, 055, 0b10101010 stb.) működnek. A szokásos aritmetikai (+, -, *, /, % (osztás maradéka), ** (hatványozás, az ES6-tól)) és bitműveletek (bitenkénti és: &, vagy: |, kizáró vagy: ^, bitek balra mozgatása: <<, bitek előjeles jobbra mozgatása: >>, bitek jobbra mozgatása, minden esetben balról 0-lal feltöltve: >>>).
  • Boolean (logikai): az igaz és hamis értéket a true ill. false kulcsszavakkal adhatjuk meg. Automatikus konverzió történik a többi adattípusról, pl. a 0, az üres string vagy az üres objektum a logikai hamis, a többi a logikai igaz értéknek felel meg. A szokásos logikai műveletek (és: &&, vagy: ||, tagadás: !).
  • String (szöveg): megadhatjuk egyszeres és kétszeres idézőjelek közé is tehetjük: a "hello world" és a 'hello world' egyenértékű. Az ES6-ban bevezetésre került egy újabb fajta megadás, melynek neve sablon szöveg (template string): ezt az egyszeres visszafelé idézőjelek közé ‘hello world` kell tenni, és behelyettesíti a változók értékét, pl. `Hello, ${name}!` (feltesszük, hogy van egy name változó, pl. let name = ’Csaba'; ez esetben az eredmény Hello, Csaba! lesz). Stringeket összefűzni a + jellel tudunk, pl. "Hello, " + "world!".
  • Object (objektum): kb. azok tartoznak ide, ami Java-ban is ide tartoznának. Minden objektum, ami nem primitív és nem függvény. A null a nem létező objektum, ami szintén a Java-hoz hasonlít.
  • Function (függvény): ezeket külön típusként kezeli a JavaScript.
  • Undefined (nem definiált): ha egy változót csak deklaráltunk, de nem adunk neki értéket, vagy még nem is deklarálunk, annak a típusa ez.

Pár példa:

console.log(typeof(2)) // number
console.log(typeof(true)) // boolean
console.log(typeof('Csaba')) // string
console.log(typeof({name:'Sanyi', age: 42})) // object
console.log(typeof(function f(a, b) {return a + b})) // function
console.log(typeof(nonexisting)) // undefined

Operátorok

A Java-ban megszokott értékadó operátorok (=, +=, -=, *=, /=, …, ++, ) a JavaScript-ben is működnek. Emlékeztetőül:

  • x += 5 (és társai) jelentése x = x + 5.
  • x++ jelentése x = x + 1. Ha az eredményt értékül adjuk, akkor Különbséget kell tenni az alábbi kettő között: y = x++ (az y értéke az x eredeti értéke lesz), és y = ++x (az y értéke az x új értéke lesz).

A JavaScript-ben is meg van a három tagú (ternary) operátor: ?:. A használatát egy példán illusztrálom: az a == b > 2 ? c : d jelentése a következő: ha b értéke nagyobb mint kettő, akkor a értéke legyen c, egyébként d. Ez egyébként csökkenti a kód olvashatóságát, így használatát jól meg kell fontolni. Megengedett akkor, ha paraméter átadásnál nem szeretnénk segédváltozót létrehozni, vagy egy if szerkezetben nem szeretnénk leírni kétszer ugyanazt, de ahol észszerű az if … else szerkezet használata, ott érdemes azt használni.

A máshol szokásos összehasonlító operátorok itt is jelen vannak: == (egyenlőség vizsgálata), != (nem egyenlő), <, >, <=, >=; mindegyik eredménye logikai igaz vagy hamis. A JavaScript speciális ebből a szempontból is. Mivel a típuskonverzió automatikus, a következő eredménye igaz lesz:

console.log(5 == "5") // true

Lehet, hogy ezt akarjuk; lehet, hogy nem. A típusos nyelvekhez hozzászokott fejlesztőként én azt várnám, hogy eleve ne is lehessen ilyet írni, de a JavaScript esetében erről lekéstünk. Mindenesetre ha azt szeretnénk, hogy csak akkor legyen az összehasonlítás eredménye igaz, ha a típusaik is megegyeznek, és az eltérő típusú értékek összehasonlításának eredménye minden esetben hamis legyen, akkor a === operátort használhatjuk (melynek tagadó párja a !==):

console.log(5 === "5") // false
console.log(5 === 5) // true
console.log(5 !== "5") // true
console.log(5 !== 5) // false

(És ezzel a nyelv tervezői - minden jó szándékuk ellenére - ismét beletettek egy aknát a rendszerbe. Más rendszerekben ugyanis az === összehasonlító operátort táblázatok oszlopainak az összehasonlítására szokták használni, melyek a tagadó párja inkább a =!=. De a jövő majd ezt is megoldja.)

Hatókör

Angolul scope. Azt jelenti, hogy az adott változó hol érhető el. Háromféle hatókört különböztetünk meg:

  • Globális: bárhonnan elérhetőek. Ha egy váltotót "csak úgy" megadunk, var, let (vagy const) nélkül, akkor az automatikusa globális lesz, akárhol is hozzuk létre.
  • Függvény: csak adott függvényen belül érhetőek el, de ott bárhonnan, akárhol is hoztuk létre őket.
  • Blokk: csak a saját blokkján belül érhetőek el.

Itt lényeges koncepcióbeli különbség van a var ill. a let és a const kulcsszavakkal létrehozott változók hatókörei között.

A var kulcsszóval létrehozott változók hatóköre csak globális és függvény lehet. Itt meg kell említeni az ún. hoisting mechanizmust: mindkét hatókör esetén, akárhol is hozzuk létre a változót, a deklarálást mindig felviszi az elejére: globális hatókör esetén a program tetejére, függvény hatókör esetén pedig a függvény fejléc alá; ez utóbbit még akkor is, ha egy belső blokkon belül történik a deklarálás. Ezt a kissé szokatlan megoldást két példán szemléltetem. Ennek megértése fontos amiatt, hogy megértsük a let és const létjogosultságát.

Ha a következőt írjuk:

console.log(a)
var a = 2

abból a szabvány szerint belül a következő lesz:

var a
console.log(a)
a = 2

Az eredmény: undefined, holott hibát várnánk.

Egy másik példa:

function f() {
    if (3 > 2) {
        var b = 3
    }
    console.log(b)
}
 
f()

Azt várnánk, hogy ne írjon ki semmit, viszont kiírja a 3-at, ugyanis a fenti az alábbira alakul át:

function f() {
    var b
    if (3 > 2) {
        b = 3
    }
    console.log(b)
}
 
f()

A probléma ezzel az, hogy szembe megy a más programozási nyelvekben (pl. Java-ban) megszokott logikával, ráadásul mivel nagyon valószínűtlen, hogy tényleg ezt szeretnénk írni, elnyel a hibákat.

A let (és const) kulcsszóval létrehozott változók (konstansok) hatóköre csak globális és blokk lehet. Ráadásul itt - a programozók által megszokott viselkedésnek megfelelően - nincs hoisting. A let használatával mindkét fenti példa hibát jelezne - nagyon helyesen! E kulcsszavak bevezetésének létjogosultságát személy szerint az alábbiakban látom:

  • A hatókörük sokkal természetesebb, mint a var esetén.
  • Minden programozási nyelvben indokolt különbséget tenni a változók és konstansok között, ez utóbbi használatával a fordító ill. futtató rendszerek optimalizálni tudják a memória használatot. A funkcionális programozási elemek bevezetése (pl. nyíl függvények (arrow function); ld. később) is indokolttá teszik a konstansok bevezetését.

Fontos viszont úgy tekintenünk erre, hogy egy alapvetően rosszul megtervezett megoldást lecseréltek egy jobbra. Helytelen lenne azt gondolni, hogy három féle hatókör van a JavaScript-ben (noha a valóságban tényleg három van), ugyanis a régi és az új világban is kettő-kettő van. A var és a let + const megoldásokat nem szabad egyszerre használni: a már meglevő komponensek tovább fejlesztésekor, melyben var-t használtak (pl. mert a fejlesztése már 2015 előtt elkezdődött) továbbra is a var kulcsszó használata az indokolt, és ez esetben kerüljük a let-et és const-ot. Ugyanakkor egy teljesen új projekt esetén célszerű a var-t kerülni, és kizárólag a let ill. const kulcsszavakat használni.

Mivel a több milliárd működő weboldal és a több milliárd feltelepített böngésző miatt szinte lehetetlen megváltoztatni a JavaScript specifikációját (azt csak bővíteni lehet), egy rossz örökségként a var jó eséllyel örök időkig benne marad. A web fejlesztők feladata annyira kikoptatni, amennyire csak lehet.

Feltételkezelés

Számomra ott kezdődik a programozási nyelv, hogy van benne feltételkezelés. A JavaScriptben van, thet programozási nyelvről van szó. (A HTML-ben egyébként nincs, az nem is programozási nyelv.)

if

A legalapvetőbb feltételkezelő szerkezet az if … else. A szintaxisa a legtöbb programozási nyelvben (pl. a Java-ban) megszokott, pl.:

let a = 5
if (a > 0) {
    console.log("pozitív")
} else if (a == 0) {
    console.log("nulla")
} else {
    console.log("negatív")
}

switch

Ha az if-nek túl sok ága van, akkor érdemes az áttekinthetőbb switch … case … default szerkezetet használni. Ez a JavaScript-ben is megtalálható, a többi programozási nyelvben megszokott szintaxissal:

let day = 4
let dayStr = "";
switch (day) {
    case 1:
        dayStr = "hétfő"
        break
    case 2:
        dayStr = "kedd"
        break
    case 3:
        dayStr = "szerda"
        break
    case 4:
        dayStr = "csütörtök"
        break
    case 5:
        dayStr = "péntek"
        break
    case 6:
        dayStr = "szombat"
        break
    case 7:
        dayStr = "vasárnap"
        break
    default:
        dayStr = "ismeretlen"
}
console.log(dayStr)

Ciklusok

for

A for (inicializálás; növelés; feltétel) ciklus a JavaScripot-ben is megtalálható:

for (let i = 1; i <= 5; i++) {
    console.log(i)
}

Valójában let nélkül is működik; ez esetben automatikusan úgy jön létre, mintha var lenne. A különbség az, hogy let nélkül az i definiált marad a ciklus után is, mégpedig 6 értékkel. Ez nem jó: a ciklus után nem szabad kihasználni a ciklus változó értékét, és az a legjobb, ha a programozási nyelv nem teszi ezt lehetővé. A let ezt a hibát is javította.

for in

A for ciklust a legritkább esetben használjuk számlálásra. Sokkal gyakoribb az, amikor egy struktúra elemein lépkedünk végig. Ez persze megvalósítható sima for ciklussal is, csakhogy annak több hátránya van:

  • Lassú. Minden egyes lépésben megcímezzük a struktúra adott elemét, melynek ideje számottevő lehet.
  • Felesleges változó. Létrehozunk egy változót, aminek csak az a szerepe, hogy végig iteráljunk az elemeken.
  • Nehezen olvasható. Ha van egy bonyolultabb ciklus, és azon belül több utasítás, akkor első ránézésre sokszor nem egyértelmű, hogy itt egy struktúra elemein történő végiglépkedésről van szó.

Mint a legtöbb programozási nyelvben, a JavaScript-ben is bevezették a for … in … struktúrát. Ez szintaxisában (sajnos) eltérő az egyes nyelvekben, szemantikájában viszont ugyanazt jelenti. Lássunk egy példát!

var person = {name:"Pista", age:40, email:"pista@email.com"}
 
for (key in person) {
    console.log(key + " = " + person[key])
}

A példában létrehoztunk egy objektumot, amiről majd később lesz szó részletesen. Ebben kulcs-érték párok szerepelnek. A for … in … valójában a kulcsokon lépked végig. Az eredménye:

name = Pista
age = 40
email = pista@email.com

Kicsit nyakatekert példával kezdtük, melynek oka van! Az, hogy a kulcsokon lépked végig, látszólag ártalmatlan, de lássunk egy másik példát is!

let cars = ["BMW", "Volkswagen", "Trabant"]
 
for (car in cars) {
    console.log(car)
}

Látszólag jónak kellene lennie, viszont van vele egy bökkenő: ha lefuttatjuk, akkor nem az autókat írja ki, hanem a következő számokat:

0
1
2

Ennek a magyarázata a következő: belül a tömböt is kulcs-érték párokként tárolja, melynek a kulcsa az index, az értéke pedig az adott indexű elem. Csakhogy azáltal, hogy a for … in … a kulcsokon lépked végig, az egyes iterációkban az in-től balra levő változó a kulcsokat, jelen esetben az indexeket fogja tartalmazni! Az elvárt működést az alábbi adja:

let cars = ["BMW", "Volkswagen", "Trabant"]
 
for (car in cars) {
    console.log(cars[car])
}

Az eredmény:

BMW
Volkswagen
Trabant

Jó ez? Szerintem nem! Azt gondolom, hogy nem elég, hogy működjön a kód, de annak olvashatónak is kell lennie. Márpedig a cars[car] szerintem nagyon félrevezető! Ugyanakkor általában is igen nehéz megváltoztatni egy programozási nyelv működését, a JavaScript esetén viszont ez konkrétan lehetetlen, hiszen több milliárd weboldal létezik, és több milliárd példányban futnak a böngészők.

for of

A fenti problémát a JavaScript szabványt karbantartók is konstatálták, és a 2015-ben kiadott szabványban (ES6) bevezettek egy másik megoldást: for … of …. Zseniális ötlet: felülről kompatibilis marad a korábbival, a felhasználók nem vesznek észre semmit a változásból, a fejlesztők életét viszont - látszólag - megkönnyítik. A szintaxisa ugyanaz, mint a for … in …-é, viszont ez már az értékeken lépked végig:

let cars = ["BMW", "Volkswagen", "Trabant"]
 
for (car of cars) {
    console.log(car)
}

Az eredmény:

BMW
Volkswagen
Trabant

Az ember gondolná, hogy milyen jó, most már elég lesz a for … of mindenre. De próbáljuk ki a korábbi példát itt is!

var person = {name:"Pista", age:40, email:"pista@email.com"}
 
for (key of person) {
    console.log(key + " = " + person[key])
}

Az eredmény kellemetlen meglepetés:

Uncaught TypeError: person is not iterable

Szóval csak látszólag könnyítették meg a fejlesztők életét, a valóság az, hogy két struktúrát is meg kell tanulniuk, ami ráadásul nagyon hasonló, így könnyű keverni. A hivatalos magyarázat (vagy inkább magyarázkodás) szerint a következő a helyzet. A struktúrák két fő csoportját különböztethetjük meg:

  • Enumerable (megszámlálható): a for … in … struktúrával járható be, ami a kulcsokon lépked végig. Mivel az enumerable tágabb, mint az iterable (ld. lejjebb), azokon a struktúrákon, amelyek megszámlálhatóak, de nem bejárhatóak (pl. egy objektum elemei), a for … of … hibát jelez.
  • Iterable (bejárható): szűkebb, mint az enumerable. A for … in … a kulcsokon, a for … of … pedig az értékeken lépked végig.

Nagy általánosságban tehát azokon a struktúrákon, amelyek megszámlálhatóak, de nem bejárhatóak, a for … in …, azokon pedig, amelyek bejárhatóak, a for … of … szerkezetet kell használni, ez a különbség tehát a kettő között, bár a fentiek tükrében ez inkább magyarázkodásnak mint magyarázatnak tűnik számomra.

while

Az elöl tesztelősnek nevezett while (feltétel) {ciklusmag} ciklus igen gyakori. A ciklus mag addig hajtódik végre, amíg a ciklus elején a feltétel igaz. Ez azt is jelenti, hogy előfordulhat, hogy egyszer sem hajódik végre. A JavaScript is tartalmazza ezt a módszert, a Java-ban megszokott szintaxissal pl.:

let i = 1
while (i <= 5) {
    console.log(i)
    i++
}

do … while

A do {ciklusmag} while (feltétel) neve hátul tesztelős ciklus jóval ritkább, mint az elöl tesztelős. Nem is minden programozási nyelvben szerepel, de a JavaScript-ben megtalálható. Ebben az esetben a ciklusmag legalább egyszer lefut, majd a végén történik a feltétel ellenőrzés, és ha a feltétel igaz, akkor ismét lefut. (Aki járatos a Pascal-ban: a do … while nem teljesen ugyanaz, mint a repeat … until, ugyanis ez utóbbi esetben addig fut a ciklus mag, amíg a feltétel hamis, míg a do … while esetben amíg igaz. Sajnos ilyen kavarodások máshol is vannak.)

let i = 1
do {
    console.log(i)
    i++
} while (i <= 5)

Ugró utasítások

goto nincs a JavaScript-ben, break és continue viszont van.

A break kilép a ciklusból. Tipikusan feltételen belül hívjuk meg:

let i = 1
while (true) {
    if (i > 5) {
        break
    }
    console.log(i)
    i++
}

A continue a ciklusmag elejére ugrik. Az alábbi példa a kiírásban kihagyja a páratlan számokat:

let i = 0
while (i < 10) {
    i++
    if (i % 2 == 1) {
        continue
    }
    console.log(i)
}

Függvények

Egyszerű függvények

Függvényeket a JavaScript-ben az alábbi szintaxissal tudunk létrehozni ill. hívni:

function add(a, b) {
    return a + b
}
 
console.log(add(3, 2))

Paraméter átadás

A JavaScript-ben - a Java-hoz hasonlóan - a primitív típusok (számok, string és logikai) érték szerint, az objektumok pedig referencia szerint adódnak át.

  • Az érték szerint átadott paraméter eredeti értéke nem változik meg. Más memóriacímre mutat ugyanis az átadott változó (ami lehet maga az érték is, nem feltétlenül változó), és a függvény paramétere. Megjegyzés: a C-ben és C++-ban van lehetőség primitív típust is referencia szerint átadni; a JavaScript-ben erre - szerencsére - nincs mód.
  • A referencia szerint átadott paraméter értéke megváltozhat. Egészen pontosan: ha maga a referenciát változtatjuk a függvényben, akkor az eredeti nem változik meg, viszont ha az objektum egy adatmezőjét, akkor igen. Az objektumokről később még lesz szó részletesebben.

Lássunk egy példát!

let person = {name: "Pista", age: 45}
let i = 5
 
function f(person, i) {
    person.name = "Sanyi"
    i = 8
}
 
console.log(person.name) // Pista
console.log(i) // 5
f(person, i)
console.log(person.name) // Sanyi
console.log(i) // 5

Az f függvény mindkét paraméterét megváltoztatja (ami egyébként egy nagyon nem szép dolog, és csak a példa kedvéért mutatom be).

  • A szám (i) nem változik: a globális i változó más memóriaterületen van, mint az f függvény i paramétere.
  • Az objektum (person) értéke megváltozik. Az igaz, hogy a globális person és a függvény paraméter person mint referencia más memóriaterületen vannak, de ugyan oda mutatnak. Tehát ha az egyik megváltoztatja a referencia által mutatott memóriaterület értékét, azt a másik is látja.

Tehát a referencia által mutatott memóriaterület tartalmát meg tudjuk változtatni, magát a referenciát azonban nem! Lássunk erre is egy példát!

let person = {name: "Pista", age: 45}
 
function f(person) {
    person = {name: "Jóska", age: 30}
}
 
console.log(person.name) // Pista
f(person)
console.log(person.name) // Pista

Ez sokszor még a tapasztalt fejlesztőknek sem egyértelmű. Hivatkozások esetén különbséget kell tennünk a hivatkozás maga (azaz a referencia) és a hivatkozott érték (a tartalom) között.

Rekurzió

A többi programozási nyelvhez hasonlóan a JavaScript függvények is meg tudják magukat hívni rekurzívan. Adott Fibonacci számnal egy nagyon nem hatékony, ám oktatási céllal megfelelő kiszámolási módja az alábbi:

function fibonacci(n) {
    if (n < 2) {
        return n
    } else {
        return fibonacci(n - 1) + fibonacci(n - 2)
    }
}
 
console.log(fibonacci(8)) // 21

Önmagát hívó függvények

Létrehozhatunk olyan függvényt is, amit azonnal meghívunk. Ez az önmagát hívó függvény; nem összetévesztendő a rekurzióval. Sok értelmét egyelőre nem látom, mindenesetre az egzotikus lehetőségek iránt érdeklődőknek íme egy példa:

(function () {
    console.log("Hello, world!")
})()

Hoisting

A változónál bemutatott hoisting függvényekre is érvényes: a függvények - akárhol is vannak ténylegesen - felkerülnek a fejlécbe. Így meg tudjuk hívni azokat a függvényeket is,a melyeket később definiálunk:

console.log(add(3, 2))
 
function add(a, b) {
    return a + b
}

Függvény kifejezések

Mivel a JavaScript funkcionális nyelv is, változónak értékül adhatjuk a függvényt, és magán a változón is végrehajthatjuk:

let add = function anAddFunction(a, b) {
    return a + b
}
 
console.log(add(3, 2))

Sőt, egy függvény lehet név nélküli (anonymous) is:

let add = function(a, b) {
    return a + b
}
 
console.log(add(3, 2))

Ez a használat szempontjából végső soron csak szintaktikai cukorka; a tapasztalatom szerint felcserélhető.

Viszont lássunk egy másik példát, ami illusztrálja az ebben rejlő lehetőségeket. Az alábbi példákban a perform függvény név, az operation pedig változó név; azok bármik lehetnek. (A let, a function és a return kulcsszavak.)

let add = function(a, b) {
    return a + b
}
 
let multiply = function(a, b) {
    return a * b
}
 
function perform(a, b, operation) {
    return operation(a, b)
}
 
console.log(perform(3, 2, add))
console.log(perform(3, 2, multiply))

Itt tehát függvényt adunk át paraméterként; a tényleges műveletet a perform hajtja végre.

Ez a korábban bemutatott szintaxissal is működik:

function add(a, b) {
    return a + b
}
 
function multiply(a, b) {
    return a * b
}
 
function perform(a, b, operation) {
    return operation(a, b)
}
 
console.log(perform(3, 2, add))
console.log(perform(3, 2, multiply))

De van egy másik, szintén gyakori szintaxis: miért adnánk egyáltalán nevet a függvények, vagy miért hoznánk létre változót, amikor csak egyszer használjuk? Most azon lehet vitázni, hogy melyik az olvashatóbb, mindenesetre jó tudni, hogy így is lehetséges:

function perform(a, b, operation) {
    return operation(a, b)
}
 
console.log(perform(3, 2, function(a, b) {
    return a + b
}))
console.log(perform(3, 2, function(a, b) {
    return a * b
}))

Nyíl függvények

A nyíl függvények (arrow function) az ES6-ban jelentek meg. Más programozási nyelvekben lambdának hívjuk ezt a szerkezetet.

Lényegét és használatát az előző példán illusztrálom. Az előző példában létre hoztunk két anonymous függvényt, amit értékül adtunk egy másik függvénynek. További szintaktikai cukorka az, hogy elhagyhatjuk a function és return kulcsszavakat, kissé átalakítva a függvényt:

function perform(a, b, operation) {
    return operation(a, b)
}
 
console.log(perform(3, 2, (a, b) => a + b))
console.log(perform(3, 2, (a, b) => a * b))

A funkcionális programozásban a lambda alapvető fontosságú. Egy paraméter esetén a paraméter listának a zárójelét se kell kitenni.

function perform(a, operation) {
    return operation(a)
}
 
console.log(perform(3, x => x + 1))
console.log(perform(3, x => 2 * x))

A Function konstruktor

Függvényeket reflection technikával a Function konstruktorral is létre tudunk hozni:

let add = Function("a", "b", "return a + b")
console.log(add(3, 2))

De remélem, senki nem vetemedik arra a szörnyű bűncselekményre, hogy így hozzon létre függvényt!

A függvények objektumok

A JavaScript-ben a függvények ugyanúgy viselkednek, mint az objektumok, amiről később lesz szó. Egy függvénynek lehetnek tulajdonságai és metódusai is. Az ilyen függvényeket konstruktor függvényeknek nevezzük, és a szokásos függvény elnevezési konvenciótól eltérően itt a konvenció az, hogy nagy betűvel írjuk ezeknek a függvényeknek a nevét. Elöljáróban álljon itt egy példa:

function MyHello(name) {
    this.name = name
 
    this.sayHello = function() {
        return `Hello, ${this.name}!`
    }
}
 
let myHello = new MyHello("Csaba")
console.log(myHello.sayHello())

Az arguments tulajdonság

Az argumentumokat az arguments tulajdonság változón keresztül is elérjük, pl.:

function findMax() {
    let max = -Infinity
    for (let i = 0; i < arguments.length; i++) {
        let actualArg = arguments[i]
        if (actualArg > max) {
            max = actualArg
        }
    }
    return max
}
 
console.log(findMax(5, 4, 8, 9, 2))

call() és apply()

Ha egy objektum függvényét egy másik objektumra vonatkoztatva szeretnénk meghívni, akkor azt a call() vagy az apply() segítségével tudjuk megtenni.

function Person(firstName, lastName, age) {
    this.firstName = firstName
    this.lastName = lastName
    this.age = age
    this.fullName = function() {
        return this.firstName + " " + this.lastName
    }
}
 
let p1 = new Person("Csaba", "Faragó", 43)
let p2 = new Person("László", "Nagy", 35)
console.log(p1.fullName.call(p2))
console.log(p1.fullName.apply(p2))

A kettő között belső technikai különbség van: a call() esetében a paramétereket egyesével kapja meg a függvény, míg apply() esetében tömbként.

Személyes véleményem az, hogy ennek nincs sok értelme, ráadásul nagyon rontja az olvashatóságot is.

Generátorok

Generátorok segítségével iterátor jellegű függvényeket tudunk létrehozni. Ezt egy példán illusztrálom. Egy olyan módszert szeretnénk megvalósítani,a mi a Fibonacci számokat számolja ki és adja vissza, egyesével. Természetesen nem szeretnénk minden egyes lépésben újra számolni. A természetes megoldás az az, hogy eltároljuk valahol (pl. globális változókban, vagy egy osztály attribútumaiként) az aktuális két számot, és lekérdezéskor léptetjük. Ugyanezt valósítja meg elegáns formában a generátor. Generátort a function* kulcsszóval tudunk létrehozni, visszatérni pedig nem a return, hanem a yield kulcsszóval kell, pl.:

function* fibonacci() {
    let n1 = 0
    let n2 = 1
    while (true) {
        yield n1
        let sum = n1 + n2
        n1 = n2
        n2 = sum
    }
}
 
const fib = fibonacci()
for (i = 0; i < 10; i++) {
    console.log(fib.next().value);
}

Ez ebben a formában szintaktikai cukorka, ami a funkcionális nyelvekben alapvető fontosságú.

Objektumorientáltság

Hosszú utat tett meg az objektumorientáltság a JavaScript-ben. Objektumokat valójában már elég régóta létre lehet hozni, viszont ezek inkább hasonlítanak asszociatív tömbökre, azaz kulcs-érték párokra, metódus hozzáadási lehetőséggel, mint az objektumorientált programozásban megszokott objektumokra.

A későbbiekben a fejlődés az objektumorientált irányba mutatott. A konstruktor függvények megjelenésével már példányosítani is lehet, valamint külön kulcsszóval tudunk lekérdezőket (getter) és beállítókat (setter) létrehozni. Statikus metódusokat is létre hozhatunk.

Az ES6-ban aztán megjelent az a módszer, amit klasszikus értelemben objektumorientált programozásnak hívunk. Itt már van öröklődés is, metódus felülírás (overriding), az osztályon belül létrehozhatunk konstruktort, és a szintaxis nagyban hasonlít a Java szintaxishoz (bár nem teljesen ugyanaz; pl. a konstruktorok, beállítók és lekérdezők esetén eltér).

Objektumok létrehozása

A kezdetleges objektumok valahogy így néztek ki:

let person = {
    firstName: "Csaba",
    lastName: "Faragó",
    age: 43
}
 
console.log(person.firstName)
console.log(person.lastName)
console.log(person.age)

Tulajdonságot utólag is adhatunk hozzá, ill. módosíthatjuk a már meglevőt:

person.country = "Hungary"

Ennek ebben a formában nincs sok köze az objektumorientáltsághoz, viszont - ahogy látni fogjuk - egész jól használható asszociatív tömbként.

Az Object osztály

Az alábbi példa a fentivel ekvivalens:

let person = new Object()
person.firstName = "Csaba"
person.lastName = "Faragó"
person.age = 43

Itt tehát létrehozunk egy üres objektumot (asszociatív tömböt) a new kulcsszóval, és utána feltöltjük adatokkal.

Tulajdonságok

A tulajdonságokat háromféleképpen adhatjuk meg, ill. hivatkozhatunk rájuk:

person.firstName = "Csaba"
person["lastName"] = "Faragó"
let ageStr = "age"
person[ageStr] = 43
  • person.firstName: ez az objektumorientált programozásban megszokott mezőnév hivatkozás.
  • person["lastName"]: ez inkább hasonlít az asszociatív tömb kulcsára.
  • person[ageStr]: ha a mezőnév egy - akár generált - stringben adott, akkor ilyen módszerrel tudunk csak hivatkozni. Az objektumorientált világban stringben adott mezőnév esetén csak reflexiót használhatnánk, de az asszociatív tömbös megadási módszernél ez is működik.

Metódusok

A JavaScript objektumok annyiban többek, mint a síma asszociatív tömbök, hogy ezek tartalmazhatnak metódusokat is, pl.:

let person = {
    firstName: "Csaba",
    lastName: "Faragó",
    age: 43,
    fullName: function() {
        return this.firstName + " " + this.lastName
    }
}
 
console.log(person.fullName())

A másik megadási mód, amikor vagy nulláról építjük fel, vagy utólag adjuk hozzá:

person.fullName = function() {
    return this.firstName + " " + this.lastName
}

Megjelenítés

Az objektum adatait többféleképpen megjeleníthetjük.

Mezőnévre történő hivatkozással, pl. person.firstName, ahogy a példákban is láthattuk

Az tulajdonságokon történő végigiterálással, a for … in struktúra segítségével:

for (property in person) {
    console.log(property + ":" + person[property])
}

Ha az objektum tartalmaz metódust, akkor ez a metódus törzsét írja ki.

Az Object osztály használatával, a kulcsokat, az értékeket ill. a kulcs-érték párokat tudjuk tömbbé (ill. ez utóbbit két dimenziós tömbök tömbjévé) alakítani, majd kiírni az alábbi módon:

console.log(Object.keys(person))
console.log(Object.values(person))
console.log(Object.entries(person))

Lekérdezők és beállítók

Az ES5-ben megjelentek a beállítók (setter) és lekérdezők (getter), a set ill. a get kulcsszavak használatával. A fenti példa ilyen átírása az alábbi:

let person = {
    _firstName: "",
    _lastName: "",
    _age: 0,
 
    get firstName() {
        return this._firstName
    },
 
    set firstName(firstName) {
        this._firstName = firstName
    },
 
    get lastName() {
        return this._lastName
    },
 
    set lastName(lastName) {
        this._lastName = lastName
    },
 
    get age() {
        return this._age
    },
 
    set age(age) {
        this._age = age
    }
}
 
person.firstName = "Csaba"
person.lastName = "Faragó"
person.age = 43
 
console.log(person.firstName)
console.log(person.lastName)
console.log(person.age)

A mezőket tehát nem közvetlenül állítjuk be, hanem a beállítókon keresztül, függvényhívással. Adódik persze a kérdés, hogy ez mire jó, hiszen ugyanaz az eredménye, mint az eredetinek, viszont sokkal hosszabb, valójában áttekinthetetlenebb is, ráadásul a mező neve nem egyezhet meg a setter nevével. Ebben a példában valóban nincs sok értelme, viszont ez egy fontos lépés az objektumorientáltság irányába. Az objektumorientált világban ugyanis az attribútumok (JavaScript terminológiával: azok a tulajdonságok, amelyek nem függvények) privátok, és azokat csak publikus metódusokon keresztül érhetjük el. Azokat a metódusokat, amelyek csak beállítják az attribútum értékét, beállítóknak (setter), azokat pedig,a melyek lekérdezik, lekérdezőknek (getter) nevezzük. Ezek a metódusok ugyanakkor más feladatot is elláthatnak, azon kívül, hogy csak beállítják ill. visszaadják az aktuális értéket, pl.:

  • Adat ellenőrzés: például az életkor esetében hibát írhatnak ki, ha az érték negatív, és figyelmeztetést, ha az érték 120-nál magasabb.
  • Adat másolás: ha az attribútum egy objektum, és lekérdezésnél csak úgy visszaadjuk, akkor a hívó fél meg tudja változtatni azt a példányt, amire az objektumunk hivatkozik. Ez megsérti az egységbe zárás (encapsulation) objektumorientált szabályt, ami szerint egy adott objektumnak az attribútumai által leírt belső állapotát kívülről közvetlenül megváltoztatni sem szabad. Tehát pl. egy tömb közvetlen visszaadása helyett a lekérdező lemásolhatja a tömböt, és a másolatot adhatja vissza.
  • Statisztika készítése: pl. meg lehet számolni, hogy hányszor történik tényleges lekérdezés ill. beállítás.
  • Hibakeresés felgyorsítása: ha tudjuk, hogy a beállításnak mindenképpen át kell futnia a beállítón, akkor a hibakereséskor elég megszerezni annak az egy utasításnak a hívási láncát; nem kell az összes létező beállítást figyelni; ez utóbbi lehetetlen küldetés akkor, ha a beállítást egy olyan külső komponens teszi, aminek még a forráskódja sem áll rendelkezésre.

Jelenleg a JavaScript-ben mindez egy eléggé hibrid állapotban van, ugyanis addig, amíg nem vezetik be a privát mezőket, addig a fentieknek nincs sok értelmük. Már van egy javaslat a privát attribútumok szintaxisára: a mezőnév elé a # karaktert kell tenni, pl. #firstName. Csakhogy az írás pillanatában ezt a böngészők még nem támogatják; ezáltal valójában az itt leírtak inkább érdekesek, mint hasznosak.

Konstruktorok

A függvényeknél már volt arról szó, hogy a JavaScript-ben a függvények és az osztályok nagyon közeli fogalmak. Pl. egy függvénynek is lehetnek "példány változói" (ami az objektumorientált attribútumnak felel meg), és tartalmazhat belső függvényeket is (ezek az objektumorientált metódusoknak megfelelő fogalom). Ha egy függvényt a new kulcsszóval hívjuk meg, akkor az már majdnem olyan, mintha osztályokat példányosítanánk, pl.:

function Person(firstName, lastName, age) {
    this.firstName = firstName
    this.lastName = lastName
    this.age = age
    this.fullName = function() {
        return this.firstName + " " + this.lastName
    }
}
 
let p = new Person("Csaba", "Faragó", 43)
console.log(p.fullName())

Az így létre hozott konstruktor függvények esetén a konvenció szerint a függvény nevét nagy kezdőbetűvel írjuk. A megvalósítás kicsit szokatlan, de a kliens (hívó) oldalon már szinte teljesen olyan, mintha igazi objektumorientált megoldást látnánk.

Megjegyzés: az alap típusoknak is vannak konstruktoraik, pl. new String(), new Boolean(), de ezek használatát célszerű kerülni,

Prototípus

Amint fent láttuk, egy objektumhoz tudunk újabb tulajdonságokat rendelni. Adott konstruktorhoz viszont ugyanazzal a szintaxissal ezt nem tudjuk megtenni (folytatva a fenti példát):

Person.country = "Hungary"
console.log(p.country) // undefined

A konstruktor függvényeknek viszont van egy ún. prototípusuk, amin keresztül már végre tudunk hajtani olyan változtatásokat, ami hatással van az összes példányra. Ezt a prototype kulcsszóval tudjuk megtenni:

Person.prototype.country = "Hungary"
console.log(p.country) // Hungary

Osztályok

A felvezetőben volt arról szó, hogy az ES6-ban megjelentek az igazi objektumorientált nyelvi elemek a JavaScript-ben. Az alábbi példa tartalmaz osztály definíciót, konstruktort, attribútumokat, metódust, öröklődést és metódus felüldefiniálást is. A példában az osztálynak két attribútuma van: vezeték név és keresztnév, és ebből képez teljes nevet. A magyar személy esetében előbb jön a vezetéknév, és utána a keresztnév.

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
 
    fullName() {
        return this.firstName + " " + this.lastName
    }
}
 
class HungarianPerson extends Person {
    constructor(firstName, lastName) {
        super(firstName, lastName)
    }
 
    fullName() {
        return this.lastName + " " + this.firstName
    }
}
 
p1 = new Person("John", "Smith")
console.log(p1.fullName()) // John Smith
 
p2 = new HungarianPerson("Csaba", "Faragó") // Faragó Csaba
console.log(p2.fullName())

Kivételkezelés

A JavaScript-ben a kivételkezelés szintaxisa sokban hasonlít a Java-ra. Lássunk egy példát!

function divide(a, b) {
    if (b == 0) {
        throw "division by zero"
    }
    return a / b
}
 
function logDivide(a, b) {
    try {
        console.log(a + "/" + b + "=" + divide(a, b))
    } catch (error) {
        console.log("Error: " + error)
    } finally {
        console.log("Division finished.")
    }
}
 
logDivide(5, 0)
logDivide(5, 2)

A divide kivételt dob, ha az osztó 0 (ld. throw). A hívó oldal a try … catch … finally struktúrával kezeli a kivételt. A finally mindenképp lefut, akár történt kivétel, akár nem. A fenti példában ennek nincs jelentősége, hiszen írhattuk volna a try … catch után is. Összetettebb esetekben, tehát ha pl. kivétel váltódik ki a catch ágban, vagy nincs is catch ág, esetleg van valahol egy return, már van jelentősége.

Habár olyan szintű kivétel hierarchia a JavaScript-ben nincs, mint a Java-ban, itt is finomhangolni tudjuk a kivételeket az Error osztály segítségével. Lássunk erre is egy példát:

class DivisionByZeroError extends Error {
    constructor(...params) {
        super(...params)
    }
}
 
function divide(a, b) {
    if (b == 0) {
        throw new DivisionByZeroError()
    }
    return a / b
}
 
function logDivide(a, b) {
    try {
        console.log(a + "/" + b + "=" + divide(a, b))
    } catch (error) {
        if (error instanceof DivisionByZeroError) {
            console.log("DivisionByZeroError")
        } else {
            console.log("Error: " + error)
        }
    } finally {
        console.log("Division finished.")
    }
}
 
logDivide(5, 0)
logDivide(5, 2)

Számos előre definiált Error osztály van: EvalError, InternalError, RangeError, ReferenceError, SytaxError, TypeError, URIError.

Modulok

A programok növekedésével megjelenik az igény a modularizálásra: arra, hogy le legyen az egész program forrása egyetlen fájlban, hanem részekre lehessen osztani. Erre a JavaScript-ben is van lehetőség, bár ez egy viszonylag új lehetőség, és a böngészők egy része még mindig nem támogatja.

A HTML forrásban is változtatni kell: meg kell adni a type="module" attribútumot:

<script type="module" src="importexample.js"></script>

A példában egy egyszerű matematikai modult hozunk létre, melyben összeadni és szorozni lehet. Az export kulcsszóval tudjuk megadni azokat, amiket lehetővé szeretnénk tenni importálásra. A fájl neve mymath.js legyen:

export {add, multiply}
 
function add(a, b) {
    return a + b
}
 
function multiply(a, b) {
    return a * b
}

Importálni az import kulcsszóval tudunk. Ennek több szintaxisa is lehetséges. Az alábbi mindent importál, amit a mymath.js exportál. A forrás neve importexample.js.

import * as mymath from './mymath.js';
 
console.log(mymath.add(3, 2))
console.log(mymath.multiply(3, 2))

Destrukturálás

A destrukturálás egy szintaktikai cukorka, amivel a kódot tömörebbé, és kellő gyakorlattal olvashatóbbá tudjuk tenni. (Bár a tapasztalatom szerint a tökélyre fejlesztett változata nem egyértelmű, azt meg kell szokni.)

Az egyik alapprobléma: tegyük fel, hogy van egy tömbünk, és annak az elemeit változükhoz szeretnénk rendelni:

let arr = [2, 3];
let a = arr[0];
let b = arr[1];
console.log(a);
console.log(b);

Kicsit bőbeszédű, az ES6-tól ezt így is írhatjuk:

let [a, b] = [2, 3];
console.log(a);
console.log(b);

Az ugyancsak ES6-ban megjelent kiterjesztés (spread) operátorral () is használható:

let [a, b, ...c] = [1, 2, 3, 4, 5];
console.log(a);
console.log(b);
console.log(c);

Ez esetben a c értéke [3, 4, 5] lesz.

Objektumokkal is működik, sőt, a gyakorlatban inkább ott használatos. Vegyük a következő példát (apropó: az unicode támogatás is az ES6-ban jelent meg):

let person = {
    firstName: "Csaba",
    lastName: "Faragó",
    age: 43
}
 
let vezetékNév = person.lastName;
let keresztNév = person.firstName;
console.log(vezetékNév);
console.log(keresztNév);

Az értékadás összevonható:

let {lastName: vezetékNév, firstName: keresztNév} = person;

Tehát a mezőnevekből tudja a JavaScript, hogy melyik új változónak mit kell értékül adni.

Most tegyük fel, hogy egy olyan függvényt szeretnénk írni, ami a személy keresztnevét és zárójelben az életkorát írja ki, ahogy egyes tévéműsorokban szokás. A hagyományos megoldás:

function printShort(person) {
    console.log(person.firstName + ' (' + person.age + ')');
}
 
printShort(person);

A destrukturált megoldás:

function printShort({firstName: firstName, age: age}) {
    console.log(firstName + ' (' + age + ')');
}

Ennek van egy továbbfejlesztett változata is, ami szintén kihasznál egy ES6-os újdonságot:

function printShort({firstName, age}) {
    console.log(firstName + ' (' + age + ')');
}

Elég tehát csak felsorolni a mezőneveket.

Belső könyvtárak

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 ho0zzá 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)

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ét 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áésidik nagyobb mint za 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 elemeket, é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 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 eleme seté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 conform() 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üt 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 az 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.localStorage.setItem("sessionKey", 50)
console.log(window.localStorage.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 automatikusa, 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 tartalmaz, 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ásodper 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 JavaScripot-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ő) }

Böngésző műveletek

TODO: fetch is

Socket

TODO: socket.on, socket.emit

AJAX

TODO: egy olyan példa, amiben van esemény hatására dinamikusan változó tartalom

Modern JavaScript

Az olyan "komoly" programozási nyelvekkel dolgozó fejlesztők, mint a Java, a C++ vagy a C#, hajlamosak lenézni a JavaScript-et (és az abban fejlesztőket), mondván, hogy az csak egy játék, nem is igazi programozási nyelv, azzal csak a baj van, és mivel a keretrendszerek úgyis többnyire elfedik a részleteket, az ebben fejlesztők tulajdonképpen nem is igazi szoftverfejlesztők. Ha megnézzük a JavaScript történelmét, korábbi állapotait, akkor ezek az állítások nem is teljesen alaptalanok.

Azonban az évtizedek során a JavaScript igencsak kicsiszolódott, és mai, modern formájában mondhatjuk, hogy a felsorolt nyelvek egyenrangú vetélytársává nőtte ki magát. Ennek az anyagnak a leírása - mely rengeteg tanulással, utána járással járt - számomra elsősorban erre mutatott rá.

Ebben a fejezetben azokat nézzük meg (részben ismét), amelyek a JavaScript-et valóban modern programozási nyelvvé teszik.

Modern JavaScript verziók

https://www.w3schools.com/js/js_versions.asp
https://babeljs.io/docs/en/learn

http://es6-features.org/

TODO ES5, ES6

Modul rendszerek

CommonJS

WebPack

https://webpack.github.io/

Babel

https://babeljs.io/

ESLint

Külső könyvtárak

TODO: letöltés vs. használat egy másik szerverről
TODO: minimális méret
TODO: jQuery, ExtJS, AngularJS, VueJS + Vuex, Mocha + Chai, Mustache
TODO: http://jsfiddle.net/
TODO: https://www.collectionsjs.com/

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License