Kategória: Web fejlesztés → JavaScript.
Table of Contents
|
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, tehát 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, amelyeket 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, ami 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.