JavaScript (tsz. JavaScripts)
A JavaScript (gyakran JS rövidítéssel) egy programozási nyelv, a világháló egyik alapvető technológiája a HTML és a CSS mellett. A weboldalak 99%-a használ valamilyen JavaScript kódot kliensoldalon a weboldal dinamikus működésének biztosítására. Minden elterjedt webböngésző beépített JavaScript-motorral rendelkezik a kliensoldali kód végrehajtására; ezeket a motorokat használják néhány kiszolgálón (szerveroldalon) és különféle alkalmazásokban is. A böngészőkön kívüli futtatáshoz jelenleg a legnépszerűbb JavaScript futtatókörnyezet a Node.js.
A JavaScript egy magas szintű, jellemzően futásidőben (just-in-time) fordított programozási nyelv, amely megfelel az ECMAScript szabványnak. Dinamikus típusosságú, prototípus-alapú objektumorientált nyelv, és a függvények benne első osztályú értékek. Több programozási paradigma támogatott: eseményvezérelt, funkcionális és imperatív stílusban is lehet benne programozni. A nyelv számos API-t biztosít szövegek, dátumok, reguláris kifejezések, alapvető adatszerkezetek kezeléséhez, valamint a böngésző Document Object Model (DOM) objektumfájának manipulálásához. Fontos megjegyezni, hogy az ECMAScript szabvány nem tartalmaz semmilyen bemeneti/kimeneti (I/O) képességet – ezeket a konkrét futtatókörnyezet (például a webböngésző) biztosítja a JavaScript számára.
Neve ellenére a JavaScript nem a Java nyelvből származik, és a hasonló szintaxis ellenére a két nyelv tervezése nagyban különbözik. Míg a Java fordított (bájtkódra forduló), statikusan típusos, osztály-alapú nyelv, addig a JavaScript értelmezett (forráskódból fut közvetlenül) vagy JIT-fordított, dinamikusan típusos és prototípus-alapú nyelv – a névrokonság csupán történelmi marketinghúzás eredménye.
Az első sikeres grafikus webböngésző, a Mosaic 1993-ban jelent meg. Fejlesztői később megalapították a Netscape vállalatot, amely 1994-ben kiadta a Netscape Navigator böngészőt – ez rövidesen a legnépszerűbb böngészővé vált. A web hőskorában a weblapok még csak statikus tartalmak voltak, nem reagáltak a felhasználói interakciókra a betöltés után. A webfejlesztői közösség szerette volna ezt leküzdeni, ezért 1995-ben a Netscape úgy döntött, hogy beágyaz egy programozási nyelvet a böngészőbe, hogy dinamikusabbá tegye a weboldalakat. Két megoldást kíséreltek meg párhuzamosan: együttműködtek a Sun Microsystemsszel a Java nyelv beépítésén, illetve felvették Brendan Eich programozót, hogy a Scheme nyelv egy változatát építse be a böngészőbe. A cél egy „mindenki által használható nyelv” volt, amely segít „a nem programozóknak is interaktív weboldalakat létrehozni”.
Végül a Netscape vezetése úgy döntött, hogy Eich egy teljesen új nyelvet hozzon létre, amelynek szintaxisa a Java-hoz hasonló (és kevésbé a Scheme-re vagy más scriptnyelvekre emlékeztet). Az új nyelv és annak értelmezője először LiveScript néven jelent meg a Netscape Navigator 2.0 béta verziójában 1995 szeptemberében, azonban a hivatalos megjelenésre – 1995 decemberében – a nevét JavaScript-re változtatták. A JavaScript név választása később sok zavart okozott, mivel azt sugallta, hogy a nyelvnek köze van a Java-hoz. Eich szerint a névváltoztatás valójában a korai dotcom korszak marketingfogása volt, mivel akkoriban a Java új és népszerű technológia volt.
A Microsoft 1995-ben mutatta be az Internet Explorer böngészőt, ami megindította a „böngészőháborút” a Netscape-pel. A JavaScript terén a Microsoft létrehozta saját változatát JScript néven. A JScript 1996-ban jelent meg (az Internet Explorer 3 részeként), és a Microsoft párhuzamosan bevezette a CSS támogatást is. A Netscape JavaScript implementációjához képest a JScript (valamint a Microsoft böngészőjének HTML és CSS értelmezése) számos eltérést mutatott. Emiatt a webfejlesztőknek nehézséget okozott, hogy oldalaik mindkét böngészőben helyesen működjenek – elterjedtek a weboldalakon a „legjobban Netscape-ben nézhető” illetve „legjobban Internet Explorer alatt működik” feliratok.
1996 novemberében a Netscape a JavaScript nyelvet benyújtotta az Ecma International szabványosítási testületnek, hogy egy hivatalos nyelvleírás szülessen, amelyet minden böngésző gyártó követhet. Ennek eredményeként 1997 júniusában megjelent az első ECMAScript szabvány (gyakorlatilag a JavaScript 1.1 standardizált változata). Ezt követte az ECMAScript 2 (1998 június) és ECMAScript 3 (1999 december) kiadása. Közben Brendan Eich visszatekintve úgy jellemezte a korabeli JavaScriptet, mint „mellékszereplő” nyelvet, amit sokan lassúnak vagy idegesítőnek tartottak, például a felugró ablakok és görgetősávban futó üzenetek miatt.
Az ECMAScript 4 tervezése 2000 körül indult, de végül nem valósult meg teljesen. A 2000-es évek elejére a Microsoft Internet Explorer piaci részesedése ~95%-ra nőtt, így a webes kliensoldali programozás de facto szabványává a Microsoft-féle JScript vált. A Microsoft egy ideig részt vett a szabványosítási folyamatban és implementált is néhány javaslatot a JScriptben, azonban később felhagyott az ECMA együttműködéssel, így az ECMAScript 4 tervezetet végül lezárták (nem adtak ki ES4 szabványt).
Az Internet Explorer korai 2000-es évekbeli egyeduralma idején a kliensoldali webfejlesztés megrekedt, érdemi nyelvi fejlesztések nélkül. A helyzet 2004-ben kezdett változni, amikor a Netscape utódjának tekinthető Mozilla kiadta a Firefox böngészőt. A Firefox kedvező fogadtatásban részesült, sok felhasználót elhódítva az Internet Explorertől. 2005-ben a Mozilla csatlakozott az Ecma International-hoz, és munkába kezdett az ECMAScript for XML (E4X) szabványon. A Mozilla együttműködött a Macromediával (később Adobe), amely az ActionScript 3 nyelvet – az ECMAScript 4 egy tervezetéből kiindulva – saját termékeiben használta. Céljuk az ActionScript 3 sztenderdizálása, azaz ECMAScript 4-ként való elfogadtatása volt. Ennek érdekében az Adobe nyílt forráskódúvá tette a Tamarin motorját. Azonban a Tamarin/ActionScript vonal túl távol állt a webböngészőkben elterjedt JavaScripttől, ráadásul a Microsoft sem működött együtt, így az ECMAScript 4 végül nem jutott el szabványosításig.
Eközben a szabványosítási folyamaton kívül, az open-source közösségben is fontos fejlemények zajlottak. 2005-ben Jesse James Garrett egy szakmai cikkben bevezette az Ajax kifejezést, amellyel olyan technológiák összességét írta le (melyek hátbone a JavaScript), amelyek segítségével az oldal háttérben tud adatokat betölteni teljes újratöltés nélkül. Ez egy JavaScript-„reneszánszhoz” vezetett: sorra jelentek meg az új, nyílt forráskódú JavaScript könyvtárak és keretrendszerek, és az ezek köré szerveződő közösségek. Ebben az időszakban jött létre többek között a jQuery, a Prototype, a Dojo Toolkit és a MooTools keretrendszer.
A Google 2008-ban mutatta be Chrome böngészőjét, amely a konkurenseknél jóval gyorsabb V8 JavaScript-motort tartalmazott. A teljesítménynövekedés kulcsa a futás közbeni, ún. JIT-fordítás volt, emiatt a többi böngésző gyártónak is át kellett alakítania JavaScript motorjait, hogy hasonló JIT optimalizációt alkalmazzanak. 2008 júliusában a különböző érdekelt felek (Mozilla, Microsoft, Google, Adobe stb.) találkozót tartottak Oslóban, és 2009 elejére megszületett a megegyezés: minden addigi releváns eredményt egyesítve közösen viszik tovább a nyelvet. Ennek eredményeképp 2009 decemberében kiadták az ECMAScript 5 szabványt, ami hosszú évek után az első jelentős frissítés volt.
2015-ben megjelent az ECMAScript 6 (más néven ES2015) szabvány, amely hosszú fejlesztési periódus után számos új funkciót és nyelvi bővítést formalizált (ilyen volt többek között a class
szintaxis, a modulok, arrow function, Promise, stb.). 2009-ben Ryan Dahl létrehozta a Node.js platformot, amely a V8 motort egy eseményvezérelt futásidejű környezettel és I/O könyvtárakkal kombinálta, lehetővé téve a JavaScript használatát a webszervereken (szerveroldalon) is. A Node.js indulása hatalmas lendületet adott a JavaScript böngészőn kívüli elterjedésének. 2018-ra a Node.js-t már több millió fejlesztő használta világszerte, és az npm csomagkezelő regisztrálta a világon a legtöbb modulcsomagot.
Időközben az ECMAScript specifikáció karbantartása nyíltan, GitHubon keresztül zajlik. Évente adnak ki új szabványkiadást (ES2016, ES2017, stb.), de a fejlesztők inkább az egyes új javasolt nyelvi funkciók státuszát követik nyomon ahelyett, hogy az “edíciószámokra” koncentrálnának. Napjainkra a JavaScript ökoszisztéma hatalmasra nőtt: rengeteg könyvtár és keretrendszer létezik, bevett programozási módszertanok alakultak ki, és a JavaScript használata a böngészőkön kívül (szervereken, asztali és mobil alkalmazásokban) is jelentős. A komplex, egyoldalas webalkalmazások (single-page application) térnyerésével párhuzamosan több nyelv jelent meg, amelyek JavaScriptre fordulnak (transpiler-ek), hogy megkönnyítsék a JavaScript nagyprojektek fejlesztését.
A “JavaScript” kifejezés az Oracle Corporation bejegyzett védjegye az USA-ban. A védjegyet eredetileg 1997-ben a Sun Microsystems jegyezte be, majd 2009-ben az Oracle-höz került a Sun felvásárlásával. 2024 szeptemberében Ryan Dahl (a Node.js alkotója) egy nyílt levelet tett közzé, amelyben az Oracle-t a JavaScript védjegy “felszabadítására” szólította fel. A kezdeményezést – melyet több mint 14 000-en írtak alá – Brendan Eich (a JavaScript eredeti megalkotója) is támogatta.
A JavaScript a web domináns kliensoldali szkriptnyelve: a weboldalak túlnyomó többségén fut kliensoldali JavaScript kód a böngészőben. A script kódot tipikusan HTML dokumentumba ágyazzák vagy onnan hívják be, és a futás során a JavaScript a DOM-on keresztül képes módosítani a HTML oldalt és reagálni a felhasználói eseményekre. Minden elterjedt böngésző rendelkezik beépített JavaScript-motorral, amely a felhasználó eszközén hajtja végre a script utasításait.
Lássunk néhány példát, milyen interaktív viselkedéseket valósítanak meg JavaScript segítségével a weboldalak:
A weboldalak több mint 80%-a használ valamilyen külső JavaScript könyvtárat vagy webes keretrendszert a kliensoldali kód részeként. Messze a legelterjedtebb a jQuery könyvtár, de sok helyen használnak más népszerű keretrendszereket is, úgymint Angular, Bootstrap, Lodash, Modernizr, React, Underscore vagy Vue.js. Gyakran több ilyen library együtt is jelen van egy oldalon – például a jQuery-t gyakran használják a Bootstrap keretrendszerrel közösen.
Létezik egy tréfás kifejezés az ilyen külső keretrendszerektől mentes megközelítésre: “Vanilla JS”. Ezt arra értik, amikor a fejlesztő nem használ semmilyen külső könyvtárat, csak “natív” JavaScript kódot az oldal interaktivitásához. (A vanilla angolul vaníliát jelent – utalva arra, hogy semmilyen extra “ízesítést” nem adnak a nyelvhez.)
A JavaScript használata a böngészőkön kívül is elterjedt. A JavaScript-motorok ma már számos egyéb szoftverben megtalálhatók, mind webszerver oldali környezetben, mind nem böngészős alkalmazásokban. Korai kísérletek történtek a JavaScript szerveroldali népszerűsítésére: például a Netscape Enterprise Server és a Microsoft Internet Information Services (IIS) is támogatott JavaScriptet a 90-es évek végén, de ezek akkoriban csak kis niche alkalmazások voltak. A szerveroldali JavaScript használat igazán a 2000-es évek végén kezdett növekedni, elsősorban a Node.js 2009-es megjelenésével és más hasonló kezdeményezésekkel.
Napjainkban számos keretrendszer segítségével készülnek asztali és mobil alkalmazások JavaScript nyelven. Ilyen például az Electron, a Cordova vagy a React Native, amelyek lehetővé teszik, hogy webes technológiákkal (JavaScript, HTML, CSS) írjanak platformfüggetlen alkalmazásokat. Emellett a Adobe Acrobat is támogat JavaScript kódot PDF dokumentumok szkripteléséhez, és a GNOME asztali környezet felületét is JavaScriptben írt bővítményekkel lehet kiegészíteni.
Az Oracle a Java Development Kit részeként korábban tartalmazott egy Nashorn nevű JavaScript motort (JDK 8-tól kezdve, jjs
parancssori értelmezővel), de ezt a JDK 15-ben eltávolították. Helyette az Oracle a GraalJS motort ajánlja, amely az OpenJDK-val is használható, és lehetővé teszi Java objektumok elérését JavaScript kódból, így Java alkalmazásokhoz futásidejű szkriptelési lehetőséget ad. JavaScriptet néha beágyazott rendszerekben (embedded devices) is alkalmaznak – tipikusan a Node.js környezet beágyazásával például IoT eszközökben.
A korai JavaScript motorok egyszerűen értelmezték (interpretálták) a forráskódot, de a modern motorok már Just-In-Time fordítást (JIT) alkalmaznak a jobb teljesítmény érdekében. Minden jelentősebb böngészőnek megvan a saját JavaScript motorja (például Chrome – V8, Firefox – SpiderMonkey, Safari – JavaScriptCore, Edge – Chakra), és a böngészőben ez a motor együttműködik a renderelő motorral a DOM és az egyéb webes API-k (pl. WebIDL) segítségével. A JavaScript motorokat ECMAScript motoroknak is nevezik, utalva a nyelvi szabvány nevére. A JavaScript motorok használata nem korlátozódik a böngészőkre: például a Google V8 motorja a Node.js futtatókörnyezet központi eleme is. A WebAssembly technológia megjelenésével pedig a böngészők JavaScript motorjai már képesek ugyanabban a sandbox környezetben WebAssembly kódot is futtatni, mint amiben a JavaScript kódot futtatják.
Egy JavaScript motor mindig egy futtatókörnyezetbe ágyazva működik – ilyen környezet például egy böngésző vagy egy önálló alkalmazás (mint a Node.js). A runtime biztosítja a környezeti funkciókat és API-kat, mint például a hálózati kommunikáció, fájlkezelés, grafikák megjelenítése, illetve lehetőséget ad más script fájlok betöltésére.
A JavaScript nyelv egy szálon futó (single-threaded). A futtatókörnyezet egy eseménysor (üzenetsor) mechanizmust használ: az egymás után érkező üzeneteket (eseményeket) sorban dolgozza fel, minden üzenethez tartozik egy callback függvény, amit a rendszer meghív. Az aktuálisan futó függvény hívási veremmel (call stack) rendelkezik a lokális változóival. Amikor egy üzenethez tartozó függvény futása befejeződik (a verem kiürül), a JavaScript motor a következő üzenetet veszi elő a sorból. Ezt a modellt eseményhuroknak (event loop) nevezik, amely végrehajtásig futtat (run-to-completion) minden üzenetet, mielőtt a következőbe belekezdene. Fontos, hogy bár egy időben csak egy szálat futtat, a JavaScript nem blokkolja a programot várakozáskor: a hosszabb műveleteket (pl. I/O műveletek, hálózati kérések) eseményekkel és visszahívásokkal (callbackekkel) kezelik, így a program más feladatokat is el tud végezni, például reagál egy egérkattintásra, miközben mondjuk egy adatbázis lekérdezés eredményére vár.
Az utóbbi években több önálló JavaScript futtatókörnyezet jelent meg a Node.js mellett. Ilyen például a Deno vagy a Bun, amelyek alternatívát kínálnak a Node.js-hez, saját megközelítéssel és kiegészítő funkciókkal.
A JavaScript szintaxisa nagyban a C programozási nyelvből merít: megtalálhatók benne a strukturált programozás alapvető vezérlési szerkezetei, például az if
-else
elágazások, a while
és do...while
ciklusok, a for
ciklus, a switch
utasítás stb.. A C-hez hasonlóan különbséget tesz kifejezések és utasítások között. A JavaScript egyik sajátossága az ún. automatikus pontosvessző-beillesztés (automatic semicolon insertion): elméletileg minden utasítás végét pontosvesszővel ;
kellene lezárni, de a JavaScript parser bizonyos szabályok alapján automatikusan behelyezi a hiányzó pontosvesszőket, így sok esetben a programozók elhagyják azokat.
A nyelv kezdetben csak függvény szintű változó-láthatóságot (scope-ot) támogatott: a var
kulcsszóval deklarált változók a deklaráló függvény egész testében láthatóak (nem pedig csak abban a blokkscope-ban, ahol a deklaráció szerepel). Az ECMAScript 2015 (ES6) változat egy fontos újítása a blokkszintű hatókör bevezetése volt: az let
és const
kulcsszavakkal deklarált változók csak abban a blokkban (kódkapcsolatban) láthatóak, ahol létrejöttek. Ezzel együtt javasolt gyakorlattá vált, hogy új kód írásakor a var
helyett mindig let
vagy const
kulcsszót használjanak a fejlesztők a tisztább hatókör-kezelés érdekében.
A JavaScript gyengén típusos nyelv, azaz automatikusan végrehajt típuskonverziókat bizonyos műveletek során annak megfelelően, hogy milyen operátort használunk. Ez sok rugalmasságot ad, de egyben furcsa eredményekhez is vezethet a kezdők számára. Néhány példa a JavaScript meglepő típuskonverzióira:
+
operátorral): ha az egyik operandus string (szöveg), a másikat is szöveggé alakítja és összefűzi. Példa: "5" + 1
eredménye "51"
(a számot sztringgé alakítja, majd összefűzi). Ugyanez tisztán számokkal: 5 + 1 = 6
.-
operátor): mindig számmá alakítja mindkét operandust, még akkor is, ha szövegek. Példa: "5" - 1
eredménye 4
(mert a "5"
sztringet 5 számmá konvertálja, majd kivonja az 1-et). Ha a konverzió nem sikerül, az eredmény NaN
(Not a Number), pl. "5" - "alma" = NaN
. +
eredménye ""
(üres string), míg + {}
eredménye ""
(mivel az üres objektum stringgé alakítva “” lesz). (Ez utóbbi érdekesség hátterében az áll, hogy a +
operátor bal oldali üres kapcsos zárójelét a JavaScript blokk kezdetnek értelmezheti, így a fenti kifejezés valójában az üres kódtömb után egy +
kifejezést lát – ennek részletei a nyelv parser működéséhez kapcsolódó sajátosságok.)false
, 0
, üres string ""
, null
, undefined
, valamint a NaN
– logikai kontextusban false
-nak számítanak; minden más érték true
-nak minősül. Például az üres tömb (
) és az üres objektum ({}
) is igaz értéknek számít feltételben.A fenti példákból is látható, hogy a JavaScript automatikus típuskonverziós szabályai összetettek, és néha kiszámíthatatlannak tűnhetnek egy kezdő számára. Emiatt a nyelvet gyakran éri kritika, de megismerve a szabályokat a fejlesztők ki tudják kerülni a problémás eseteket.
A JavaScript dinamikusan típusos nyelv, azaz a változóknak nincs fix típusa: a változóba tetszőleges típusú érték kerülhet és a futás során akár cserélődhet is a típusa. Például egy változót induláskor számértékkel hozhatunk létre, majd később ugyanebbe a változóba egy szöveget is tárolhatunk. A típuskényszer hiánya miatt a nyelv rugalmas, de nagyobb figyelmet igényel a fejlesztőtől (például futásidőben érdemes ellenőrizni az értékek típusát bizonyos műveletek előtt). A nyelv több eszközt kínál a típusok ellenőrzésére, például a typeof
operátor visszaadja egy érték típusát (stringként), továbbá az instanceof operátorral ellenőrizhető, hogy egy objektum egy adott konstruktor/prototípus alapján jött-e létre.
A JavaScript támogatja a futásidejű kódfeldolgozást. Ennek legismertebb eszköze az eval()
függvény, amely egy string paraméterben kapott JavaScript kódot futtat le a programban. Ez extrém módon dinamikussá teheti a kódot (pl. futás közben generált kódrészleteket hajt végre), azonban az eval
használata általában nem javasolt, mert potenciális biztonsági kockázatot jelenthet és lassíthatja a programot (a JIT optimalizációk nem tudnak érvényesülni). Modern JavaScript-ben az eval
helyett biztonságosabb megoldások (például JSON.parse
JSON feldolgozásra stb.) használata ajánlott, illetve modulrendszerek segítségével kerülhető el, hogy futásidőben kelljen kódrészleteket értelmezni.
A JavaScript az objektumorientált szemléletet prototípus-alapú örökléssel valósítja meg. Míg sok más nyelvben az osztályok a központi elemek az öröklődéshez, JavaScriptben nincsenek valódi osztályok: ehelyett minden objektum egy másik objektumtól örököl tulajdonságokat, úgynevezett prototípus láncon keresztül. Minden JavaScript objektumnak van egy rejtett (belső) prototype hivatkozása, ami egy másik objektumra mutat. Ha hozzáférünk egy objektum egy adott tulajdonságához, de az objektumnak nincs ilyen mezője, akkor a nyelv automatikusan a prototípusában keresi azt, majd annak a prototípusában, és így tovább (prototípus lánc). E mechanizmus révén valósul meg az öröklés: lényegében objektumok örökölnek objektumoktól, nem pedig példányok osztályoktól. Ez eltér a klasszikus OOP-tól, de ugyanazt a célt szolgálja.
JavaScriptben bármely objektum prototípusául kijelölhetünk egy másik objektumot, így akár futásidőben is „örökíthetünk” tulajdonságokat. Új objektumot legegyszerűbben literál szintaxissal ({}
) vagy konstruktor függvényekkel hozhatunk létre. Konstruktor függvénynek nevezünk bármely függvényt, amit a new
kulcsszóval hívunk meg – ekkor a függvény egy új objektumot hoz létre és azt adja vissza. A konstruktor függvények prototípusát (FunctionName.prototype
tulajdonságát) beállítva határozhatjuk meg, hogy az így létrejövő új objektumok milyen prototípust fognak örökölni.
2015-ben az ECMAScript 6 bevezette a kényelmesebb osztály szintaxist. A class
kulcsszóval immár deklarálhatunk egy pseudo-osztályt, megadhatjuk a constructor metódust, és az extends
kulcsszóval egy „szülő” osztályt is, így az öröklődést deklaratív módon jelezhetjük. A valóságban a háttérben továbbra is a prototípusos mechanizmus működik, a class
csupán szintaktikus cukor, ami ember számára olvashatóbb formába csomagolja a prototípus-alapú öröklést. Az alábbi példa szemlélteti a class szintaxist:
class Person {
constructor(name) {
this.name = name;
}
}
class Student extends Person {
constructor(name, id) {
super(name);
this.id = id;
}
}
const bob = new Student("Robert", 12345);
console.log(bob.name); // Robert
A fenti Person
és Student
osztály definíciók és öröklés valójában a következő hagyományos JavaScript kóddal egyenértékű:
// Ez a kód teljesen egyenértékű az előző snippettel
function Person(name) {
this.name = name;
}
function Student(name, id) {
Person.call(this, name); // Meghívja a Person konstruktort az aktuális objektumra
this.id = id;
}
var bob = new Student("Robert", 12345);
console.log(bob.name); // Robert
Mindkét esetben a Student
“örökli” a Person
tulajdonságait: a prototípus lánc révén egy Student
példány instanceof Person
is egyben. Látható, hogy class nélkül is elérhető ugyanaz a funkció prototípusok használatával. Megjegyzés: a JavaScript beépített objektumai (pl. Array
, Date
, stb.) szintén prototípusokon keresztül valósítják meg funkcionalitásukat. Bár megtehetjük, hogy módosítjuk ezeknek a beépített objektumoknak a prototípusát (például az Array.prototype
-hoz új metódust adunk), általában rossz gyakorlatnak tartják a beépített prototípusok kiterjesztését, mert más külső könyvtárak is számíthatnak az eredeti viselkedésre – egy globális prototípus módosítás nem várt mellékhatásokat okozhat.
Metódusok: JavaScriptben nincs önálló szintaktikai elem a „metódusra” – egy objektum metódusa csupán egy függvény érték, amelyet egy objektum tulajdonságaként tárolunk. A különbség a normál függvény és a metódus között abban nyilvánul meg, ahogyan hívjuk őket: ha egy függvényt objektum metódusaként hívunk meg (pl. obj.fuggvenyNev()
formában), akkor a függvényen belüli this
kulcsszó arra az objektumra fog mutatni, amelyiken meghívtuk. Ha ugyanazt a függvényt önállóan (nem egy objektumhoz kötve) hívnánk, a this
másra (globális objektumra, vagy strict módban undefined-ra) utalna. Ebből következik, hogy a JavaScriptben a this
kontextus dinamikus, és híváskor dől el – szükség esetén a call()
vagy apply()
metódusokkal át is irányítható tetszőleges objektumra.
A JavaScript a kezdetektől fogva támogatja a funkcionális programozási paradigmát bizonyos mértékig. A függvények első osztályú értékek a nyelvben: ugyanúgy lehet őket változóban tárolni, paraméterként átadni vagy visszatérési értékként használni, mint bármely más adatot. Sőt, minden függvény valójában egy speciális objektum, amelynek lehetnek saját tulajdonságai (pl. egy függvénynek is lehetnek mezői vagy akár más beágyazott függvényei). Emiatt a beépített funkciók között megtalálható néhány, kifejezetten függvények kezelésére szolgáló metódus, mint a .call()
, .apply()
vagy a .bind()
, amelyekkel a függvények meghívási kontextusát és paraméterezését rugalmasan kezelhetjük.
Lexikális zárók (closure): A JavaScript híres a lexical scope alapján működő closure mechanizmusáról. Ha egy függvényen belül egy másik függvényt definiálunk, akkor a belső (beágyazott) függvény lezárja a külső függvény környezetét: a belső függvényben is elérhetőek maradnak a külső függvény lokális változói és paraméterei, még azután is, hogy a külső függvény már lefutott. Ez azért lehetséges, mert a JavaScript belső reprezentációjában a belső függvény hordoz magával egy hivatkozást a környezeti (lexikális) változókra. A closure-ök révén erőteljes technikák valósíthatók meg, például privát változók egy függvényen belül, vagy függvénygyárak (olyan függvények, amik paraméterezett belső függvényt adnak vissza).
Névtelen függvények: JavaScriptben a függvény literáloknak nem kötelező nevet adni – beszélhetünk anonim függvényekről. Ezeket sokszor eseménykezelők vagy kollekciós műveletek (pl. array.map(...)
) átadására használják, ahol nincs szükség külön névre, hisz csak egyszer definiáljuk és adjuk át őket egy másik függvénynek paraméterként. Az ECMAScript 2015 óta elérhető arrow függvények is jellemzően névtelen függvények (bár változóba elmenthetők): lényegesen rövidebb szintaxist kínálnak, és a this
értékét a környezettől öröklik (nem hoznak létre saját this
-t), ami bizonyos esetekben megkönnyíti a használatukat. Például: nums.map(x => x * 2)
– itt az x => x * 2
egy arrow függvény, mely minden elemre végrehajt egy műveletet és visszaadja az új értéket.
A JavaScript támogatja az objektumok közti delegálást is, mind implicit, mind explicit formában. A már említett prototípuslánc az implicit delegáció egy formája: ha egy objektum nem tartalmaz egy keresett metódust, a nyelv automatikusan „delegálja” a keresést a prototípusra. Emellett explicit módon is megvalósítható delegáció: például a call()
vagy apply()
használatával tetszőleges objektumot állíthatunk be egy függvény meghívásakor kontextusként, ezzel mintegy „ráruházva” azt a funkciót arra az objektumra.
Bizonyos haladó tervezési minták is kivitelezhetők JavaScriptben a delegáció révén, például a mixinek vagy a traits (szerepek) alkalmazása. Ezek lényege, hogy különálló, független modulokban (függvényekben) definiálunk közös funkciókat, majd ezeket a modulokat különböző objektumokhoz hozzácsatoljuk (delegáljuk), így további képességekkel ruházva fel azokat az objektumokat. A nyelv natívan nem támogatja a többszörös öröklődést, de mixinek segítségével elérhető, hogy egy objektum több forrásból is kapjon metódusokat.
undefined
értéket kapnak. A függvényen belül az arguments nevű pszeudo-tömb segítségével hozzá lehet férni az összes átadott argumentumhoz, függetlenül attól, hogy a deklarációban hány paraméter szerepel. (ES6 óta létezik a “rest” paraméter is (...args
szintaxis), ami elegánsabb módot ad a változó számú paraméterek kezelésére.)
egy üres tömb literál,
egy kételemű tömb, illetve {}
egy üres objektum literál, {"nev": "Alice", "kor": 25}
egy objektum két tulajdonsággal. Ezek a literálok rendkívül fontosak, hiszen a JSON (JavaScript Object Notation) formátum – amely egy széles körben használt adatcsere formátum – lényegében a JavaScript objektum- és tömb-literál szintaxisán alapul.RegExp
objektum típust, illetve a regex literál szintaxist (/minta/
) használva egyszerűen lehet bonyolult keresési mintákat definiálni és futtatni szövegeken..then()
metódussal). A nyelv kínál néhány kombinatív Promise-kezelő metódust is (Promise.all, Promise.race stb.), amelyek több párhuzamos aszinkron művelet eredményeit képesek összefogni. Az ECMAScript 2017 tovább bővítette az aszinkron programozási eszköztárat az async/await kulcsszavakkal. Az async
jelzővel ellátott függvények belsejében használható await
kulcsszó, amely megvárja egy Promise teljesülését, miközben nem blokkolja a fő szálat. Ezzel az aszinkron, nem blokkoló kódot úgy lehet megírni, mintha szinkron lenne, ami olvashatóbbá és karbantarthatóbbá teszi az ilyen kódot.function(x) expr
alakban, megelőzve az arrow függvényeket) egyes implementációkban. Mára ezek a böngészőspecifikus nyelvi bővítések kikoptak, a hangsúly a szabványos ECMAScript képességeken van.
A JavaScript nyelvben változókat a var
, let
vagy const
kulcsszóval definiálhatunk (de külön kulcsszó nélkül is létrejöhet változó, ami a globális térben jön létre – ezt azonban érdemes elkerülni, főleg “szigorú mód” alatt). Az ES2015 óta preferált a let
és const
használata a var
helyett, mivel ezek blokkszintű hatókört biztosítanak, míg a var
csak függvényszintűt.
Az alábbiakban egy példa látható a JavaScript szintaxisára és változókezelésére, kommentárokkal együtt:
// Egy függvényhatókörű változót deklarál `x` néven, és implikáltan az
// undefined speciális értéket rendeli hozzá. (Minden inicializálatlan
// változó undefined-ként kezdődik.)
// Általában kerülendő a var használata, a let és const preferált.
var x;
// Változót manuálisan is beállíthatunk undefined értékre:
let x2 = undefined;
// Deklarál egy blokkszintű változót `y` néven, és azt implicit módon
// undefined-re állítja. (A `let` kulcsszót az ECMAScript 2015 vezette be.)
let y;
// Deklarál egy blokkszintű, át nem értékelhető (constans) változót `z` néven,
// és értékül ad neki egy sztring literált. A `const` kulcsszót szintén az
// ECMAScript 2015 vezette be, és használata jelzi, hogy a változó
// értékét nem lehet újra hozzárendelni (állandó marad).
const z = "ez az érték nem írható felül!";
// Globális hatókörű változó deklarálása értékadással. (Ez rossz gyakorlat,
// és szigorú módban hibát eredményezne.)
t = 3;
// Deklarál egy `myNumber` nevű változót és értékül ad neki egy szám literált (2-t).
let myNumber = 2;
// Új értéket rendel a `myNumber`-höz, mégpedig egy sztring literált ("foo").
// A JavaScript dinamikusan típusos nyelv, ezért ez megengedett (a szám helyett most szöveg az érték).
myNumber = "foo";
A fenti példában megjegyzéseket (//
után) fűztünk a kódhoz. Látható, hogy a JavaScript egyes sor végi megjegyzéseit dupla perjellel kezdjük. (Többsoros kommenthez /* ... */
jelölés használható.) További példák és szintaxisleírás található a Wikikönyvek JavaScript szintaxis példák oldalán is.
A JavaScript lehetővé teszi, hogy a weboldalak interaktívvá váljanak, de ugyanakkor megnyit bizonyos biztonsági kockázatokat is. Rosszindulatú felek kihasználhatják a böngészőben futó JavaScript kódot arra, hogy a felhasználó gépén nemkívánatos műveleteket végezzenek. A böngésző fejlesztők több védelmi korlátozást építettek be ennek minimalizálására. Sandbox (homokozó) környezetben fut a JavaScript: azaz a script csak a webes tartalmakhoz fér hozzá, nem tud tetszőleges fájlokat módosítani a kliens gépén. Továbbá érvényesül a same-origin policy (azonos eredet elve): egy weboldalról származó script nem férhet hozzá egy másik weboldal adataihoz (pl. cookie-khoz, DOM-hoz), ezzel megelőzve, hogy eltérő domainekről érkező kód egymás adatait lopja vagy módosítsa. A JavaScript biztonsági hibáinak nagy része e két korlátozás megkerüléséből vagy megkerülhetőségéből adódik.
Léteznek továbbá korlátozott funkcionalitású JavaScript változatok (subset-ek), mint az ADsafe vagy a Secure ECMAScript (SES), amelyek szigorúbb környezetben futnak, és harmadik féltől származó kód (például reklámszkriptek) biztonságosabb futtatását teszik lehetővé. Ilyen például a Google Caja projektje is, amely sandboxolt környezetet biztosít külső JavaScript/HTML kódnak. Modern webalkalmazásoknál a Content Security Policy (CSP) bevezetése az ajánlott módszer, amely deklaratívan megszabja, milyen forrásból származó script futhat az oldalon – ezzel megelőzhető, hogy illetéktelen vagy injektált kód fusson le.
Az egyik leggyakoribb biztonsági rés a JavaScript kapcsán az ún. cross-site scripting (XSS) támadás, ami a same-origin policy megsértésére épül. XSS során a támadó eléri, hogy egy cél weboldal (például egy banki oldal) olyan kártékony JavaScript kódot szolgáltasson a felhasználónak, amelyet nem a bank írt, hanem a támadó injektált valamilyen módon. Ha a támadónak sikerül ártalmas scriptet becsempésznie az oldal kódjába (például egy sebezhető bemeneti mezőn vagy adatbázison keresztül), akkor ez a script a felhasználó böngészőjében a banki alkalmazás kontextusában fut, így hozzáférhet a felhasználó adataihoz, vagy műveleteket indíthat a felhasználó nevében (pl. pénz átutalása engedély nélkül). Az XSS támadások kivédésének legfontosabb módszere a bemenő adatok megszűrése és kódolása (HTML sanitization), hogy a felhasználó által bevitt szöveg ne tartalmazhasson aktív scriptet. Emellett a modern keretrendszerek és a szerveroldali ellenőrzések is segítenek megakadályozni, hogy injektált kód kerüljön a kimenetbe.
Bizonyos böngészők nyújtanak részleges védelmet a visszaverődő XSS támadások ellen (reflected XSS), amikor a kártékony script egy manipulált URL-ből érkezik. Ugyanakkor ez sem véd minden esetben – az XSS olyan formái ellen, ahol a kártékony kód az adatbázisban tárolódik (persistent XSS), csak a webalkalmazás helyes szerveroldali tervezése és az adatok megfelelő kezelése nyújthat védelmet.
Egy másik gyakori webes támadási forma a cross-site request forgery (CSRF), magyarul akár keresztoldali kérelem-hamisításnak is nevezik. A CSRF lényege, hogy a felhasználó böngészőjét egy másik, támadó oldalról ráveszik, hogy egy előre nem szándékolt kérést intézzen egy harmadik fél (pl. bank) weboldalához, ahol a felhasználó esetleg be van jelentkezve. Mivel a böngésző automatikusan küldi a sütiket is a kéréssel, a bank szervere azt hiszi, a kérést a felhasználó küldte szándékosan, és végrehajthatja (például átutalás a támadó számlájára). Az ilyen támadások ellen a szerverszintű védelem a fontos: véletlenszerű tokeneket (rejtett mezőket) kell beépíteni az űrlapokba, és a szerver oldalon ellenőrizni, hogy a beérkező kritikus kérések tartalmazzák-e a megfelelő tokent. Ezen felül a HTTP Referrer header vizsgálata is segíthet detektálni a más domainről érkező kéretlen kéréseket.
A CSRF egy speciális esete a “JavaScript hijacking”, amikor egy támadó oldal <script>
tag segítségével hív meg egy olyan URL-t az áldozat domainjén, ami JavaScript/JSON adatot ad vissza (pl. privát adatokat). Mivel a <script>
kéréseket nem korlátozza a same-origin policy olvasásra (JSONP működéshez), ilyenkor a támadó oldala hozzáférhet az áldozat adatához. Az ilyen jellegű támadások ellen hasonló védelmeket lehet alkalmazni, mint a CSRF ellen (tokenek, ellenőrzések).
Webalkalmazások fejlesztésénél alapelv, hogy a kliensoldali kód nem megbízható. Mivel a JavaScript kód forrását a böngészőnek le kell töltenie, egy hozzáértő felhasználó vagy támadó bármikor megvizsgálhatja vagy módosíthatja azt (például a böngésző fejlesztői eszközein keresztül). Néhány következmény:
__proto__
kulcs segítségével), akkor tetszőleges tulajdonságokat írhat felül a programban globálisan. Ez a sérülékenység abból fakad, hogy a JavaScriptben a prototípus is módosítható futásidőben, és hibás ellenőrzés esetén erre lehetősége nyílhat egy támadónak.
A modern webfejlesztésben elterjedtek a csomagkezelő rendszerek (npm, Yarn, Bower stb.), amelyekkel a fejlesztők könnyen telepíthetnek kész könyvtárakat, modulokat a programjaikhoz. Ezzel azonban a saját alkalmazásuk biztonságát részben rá bízzák ezeknek a külső könyvtáraknak a karbantartóira. Sajnos a túlzott bizalom sebezhetőségekhez vezethet. Ha egy széles körben használt könyvtár új verziójában hiba vagy biztonsági rés jelenik meg, az automatikusan az összes azt használó alkalmazást érinti. Az ellenkezője is gond: ha egy könyvtár fejlesztése leáll, és évekig nem frissül, akkor időközben felfedezett sebezhetőségeket hordozhat magában.
Egy nagyszabású felmérésben, amely 133 000 weboldalt elemzett, a kutatók azt találták, hogy az oldalak 37%-a tartalmazott legalább egy olyan JavaScript könyvtárat, amelyben ismert biztonsági hiba volt. Továbbá megállapították, hogy az átlagos elmaradás a használt könyvtárak verzióinál 1177 nap volt a legfrissebb verzióhoz képest, vagyis sok weboldal évekig nem frissítette a beépített JS keretrendszereit. Végül extrém eset is előfordult: 2016 márciusában egy Azer Koçulu nevű fejlesztő eltávolította az általa karbantartott egyik apró, de sok más csomag által használt modulját az npm tárolóból. Ennek hatására rengeteg projekt (több tízezer weboldal és program) működése hibásodott meg, amíg a csomag helyreállítása meg nem történt. Ez az eset ráirányította a figyelmet arra, hogy a külső függőségeket gondosabban kell kezelni (lockfile-ok használata, saját tükrök fenntartása, stb.), és nem szabad vakon megbízni abban, hogy mindig elérhetők és biztonságosak maradnak.
A JavaScript széles körű hozzáférést kap a böngésző funkcionalitásához (DOM manipuláció, grafika, hálózat a limitált módon, stb.). Ha a böngésző ezen komponenseiben (vagy a natív pluginokban) van sérülékenység, azt egy JavaScriptből kiinduló támadás kihasználhatja. Például korábban ismertek olyan puffer túlcsordulás jellegű hibák böngészőkben, amelyeket kihasználva a támadók tetszőleges natív kódot futtathattak a felhasználó gépén. Ezek nem a JavaScript nyelv hibái, hanem a böngésző implementációjáé, de JavaScript indította el a kihasználásukat. Történetileg több nagy böngésző is szembesült ilyen sebezhetőségekkel: találtak kritikus hibákat például a Firefox, az Internet Explorer és a Safari JavaScript motorjában is. A böngésző beépülő modulok (mint a Flash Player, vagy az ActiveX vezérlők) szintén tartalmaztak kihasználható hibákat, amelyeket JavaScriptből lehetett triggerelni, mielőtt ezeket foltozták vagy kivezették. Az Internet Explorer védelmére a Vista operációs rendszerben a Microsoft bevezette, hogy a böngésző csak korlátozott jogosultságokkal fusson (Protected Mode), így egy esetleges exploit kevesebb kárt okozhasson. Hasonlóképp a Chrome is sandbox-ba zárja a weboldalakat futtató folyamatait.
Elvben a böngészők gondoskodnak róla, hogy a letöltött webes JavaScript kód csak a korlátozott környezetben fusson, ne férjen hozzá a fájlrendszerhez vagy más rendszerszintű erőforráshoz. Sajnos előfordult, hogy ezekben a sandbox megoldásokban is találtak hibát. Mind az Internet Explorerben, mind a Firefoxban akadt példa arra, hogy hibásan túl magas jogosultságot kapott egy weboldal szkriptje. Az Internet Explorer biztonsági frissítésekben csökkentette is a JScript engine jogosultságait (pl. a Windows XP SP2-ben). Emellett a Windows operációs rendszer egy szolgáltatása, a Windows Script Host (WSH) lehetővé teszi, hogy .js
fájlokat a felhasználó közvetlenül futtasson (dupla kattintással) a gépén, mintha azok batch vagy VBScript fájlok lennének. Ilyenkor a script teljes hozzáféréssel fut a gépen, nem sandboxban. Ez azt jelenti, hogy elméletben JavaScript nyelven is írható trójai program vagy vírus, ami a WSH-t használja futtatásra – bár a gyakorlatban az ilyen JavaScript malware ritka, mert más nyelvek (pl. Visual Basic script) elterjedtebbek voltak erre a célra.
2015-ben biztonsági kutatók bemutatták, hogy egy speciális, memóriabusz sebezhetőséget kihasználó támadás (rowhammer) megvalósítható JavaScript kóddal is, böngészőben futtatva bizonyítva a koncepciót. 2017-ben publikáltak egy JavaScript alapú támadást, amely a memóriacímterület véletlenszerűsítését (ASLR) tudta megkerülni a CPU cache sajátságait kihasználva (“ASLR⊕Cache”, röviden AnC). 2018 elején pedig a Spectre nevű, processzorok spekulatív végrehajtását érintő sebezhetőség egyik kihasználási módjaként demonstrálták, hogy tiszta JavaScript kóddal is ki lehet nyerni adatokat a processzor gyorsítótárából. Ezek a támadások mind arra példa, hogy a JavaScript kód (mint általános célú programozási nyelv, még ha sandboxban fut is) elegendően alacsony szintű műveleteket tud végezni (pl. intenzív memóriahozzáférés) ahhoz, hogy hardverszintű biztonsági résekre is hatással legyen.
A JavaScript ökoszisztémában számos eszköz segíti a fejlesztők munkáját:
Gyakori félreértés, hogy a JavaScriptnek köze van a Java programozási nyelvhez – valójában csak a nevükben és részben a C-szerű szintaxisban osztoznak. Mindkét nyelv 1995-ben jelent meg, de teljesen eltérő környezetben és céllal: a Javát James Gosling fejlesztette ki a Sun Microsystems-nél, míg a JavaScript Brendan Eich a Netscape-nél. A Java egy fordított, osztály alapú, statikusan típusos nyelv, míg a JavaScript értelmezett/JIT, prototípus-alapú és dinamikusan típusos. Java-ban a kód bájtkód formájában fut a Java Virtuális Gépen (JVM), JavaScriptben a forráskódot közvetlenül a JavaScript motor futtatja. A Java objektumorientációja osztályokra és öröklési hierarchiára épül, JavaScriptben ehelyett prototípus lánc van. További különbség, hogy Java a 8-as verzióig nem tartalmazott funkcionális programozási elemeket (lambda kifejezéseket), míg a JavaScript kezdettől fogva támogatja a függvényeket mint első osztályú objektumokat (ez részben a Scheme nyelv hatása). Összességében tehát a hasonló név ellenére a két nyelv más filozófiát követ és más területen erős: a Java inkább nagy, komplex, szerveroldali rendszerekhez és Android fejlesztéshez használatos, míg a JavaScript a böngészők világában és a frontenden (újabban pedig szerveroldalon is) nélkülözhetetlen.
A JSON a JavaScript Object Notation rövidítése, egy könnyen olvasható, szöveges adatformátum, amely JavaScript objektumok literál szintaxisán alapul. Például egy JSON formátumú adat:
{ "nev": "Alice", "kor": 25, "hobbi": }
Ez gyakorlatilag megegyezik egy JavaScript objektum literállal. A JSON azonban nyelvfüggetlen formátum, számos programozási nyelv támogatja (képes JSON-t generálni és parse-olni). A weben a JSON mára az egyik legelterjedtebb adatcsere formátum (leváltva az XML-t sok területen), köszönhetően tömörségének és egyszerűségének. A JavaScript beépítetten támogatja a JSON-t a JSON.stringify()
(objektum -> JSON sztring) és JSON.parse()
(JSON sztring -> objektum) függvényekkel.
Az utóbbi években, ahogy a webalkalmazások egyre komplexebbek lettek, kialakult az igény olyan nyelvekre vagy szintaxisokra, amelyek JavaScriptre fordulnak le (azaz a forráskódból JavaScript kódot generálnak – ezt hívják transpile-nak). Ennek több oka lehet: jobb fejlesztői élmény (például statikus típusosság bevezetése), tisztább szintaxis, vagy éppenséggel a régi böngészők támogatása újabb nyelvi elemek használata mellett (polyfill/transpile kombinációval).
Két ismert transpilernyelv a TypeScript és a CoffeeScript. A TypeScript a Microsoft által fejlesztett nyelv, ami lényegében JavaScript kiterjesztése statikus típusokkal és objektumorientált elemekkel. A TypeScript forráskód .ts
fájlokban íródik, majd egy fordító JavaScript kódot generál belőle – ezt futtatja aztán a böngésző vagy Node.js. A CoffeeScript ezzel szemben egy szintaktikus cukorkákkal dúsított, Python/Ruby-szerű szintaxist vezetett be, amelyről a fordító nagyon tömör, tiszta JavaScriptet állít elő. Bár a CoffeeScript népszerűsége mára csökkent, a TypeScript széles körben használt a nagyobb projektekben a jobb karbantarthatóság miatt.
Számos egyéb nyelv vagy nyelvi újítás készült JavaScript-re fordításra (transpilerként) – például a Babel nevű eszköz modul bármilyen modern JavaScript (ES6+) kódot le tud fordítani visszafelé kompatibilis ES5 kódra, így a legújabb nyelvi funkciók régebbi környezetben is használhatók. A transpiler-ek használata ma már bevett része a frontend fejlesztési folyamatnak.
A WebAssembly (WASM) egy viszonylag új technológia, tulajdonképpen egy bájtkód formátum, amelyet a böngészők úgy terveztek, hogy kiegészítse a JavaScriptet, főként annak teljesítménykritikus területein. A WebAssembly kódot tipikusan C/C++/Rust vagy más alacsony szintű nyelvekből fordítják, és a böngésző natívan, nagy sebességgel futtatja egy sandboxolt virtuális gépben. Minden nagyobb böngésző JavaScript motorja támogatja a WASM kód futtatását, és a WASM ugyanabban a homokozóban működik, mint a JavaScript (tehát nem léphet ki a webes biztonsági modellből). A JavaScript és WebAssembly jól együttműködnek: JavaScriptből meg lehet hívni a WASM modulok funkcionalitását és fordítva, így a fejlesztők eldönthetik, mit érdemes JavaScriptben és mit érdemes WebAssembly-ben megírni.
A WebAssembly megszületését megelőzően a JavaScript motorok számára készült egy asm.js nevű kezdeményezés – ez a JavaScript nyelv egy szűk, alacsony szintű részhalmaza volt (lényegében egy stílus, amit a fordítók betartottak), amelyet a motorok optimalizálni tudtak majdnem natív sebességű futtatásra. Az asm.js a WebAssembly előfutárának tekinthető, de mára a WASM vette át a szerepét, mint szabványos megoldás a nagy teljesítmény-igényű webes számításokra.