A JavaScript-ben a függvények alapvető szerepet játszanak a programozásban, hiszen lehetőséget adnak arra, hogy a kódot logikailag elkülönítsük és újra felhasználjuk. A függvények létrehozásának és használatának számos különböző módja létezik. A függvények lehetnek deklarációk, amelyek egy teljes kódsort alkotnak, vagy kifejezések, amelyek mindig más kódrészletekhez, például változóhoz vagy más függvényekhez kapcsolódnak. Ez a különbség meghatározza a kód szintaktikai felépítését és működését.

A függvénydeklarációk a következő alapvető szerkezetet követik: a function kulcsszó, ezt követi a kötelező függvénynév és egy zárójelek közötti paraméterlista, amelyek opcionálisan tartalmazhatnak paramétereket. A függvény törzse, amely lehet üres is, mindig egy nyitó és záró kapcsos zárójelek között van. Fontos megjegyezni, hogy a függvénydeklarációnak mindig saját állításnak kell lennie a JavaScript kódban, tehát nem lehet egyszerű kifejezés. Ezt a különbséget a következő példában láthatjuk:

javascript
function samurai() {
return "samurai here"; } function ninja() { function hiddenNinja() { return "ninja here"; } return hiddenNinja(); }

Ebben a példában észrevehetjük, hogy egy függvényt másik függvényen belül definiálunk, ami JavaScript-ben teljesen normális. Az ilyen típusú elrendezés a JavaScript függvények fontosságát hangsúlyozza, és arra is utal, hogy a JavaScript rendkívül rugalmas, amikor a függvények szerepéről van szó.

A függvények másik típusa a függvénykifejezés. Mivel a JavaScript-ben a függvények elsőrendű objektumok, ugyanúgy lehet őket változókhoz rendelni, más függvények paramétereként használni, vagy más értékekkel visszaadni. A függvények tehát nemcsak az alapszintű deklarációkban, hanem kifejezésekként is szerepelhetnek. Például:

javascript
const myFunc = function() {};

A fenti példában a myFunc egy függvénykifejezés, amelyet egy változóhoz rendelünk. Ez a kód szintaktikai értelemben nem egy függvénydeklaráció, hanem egy olyan kifejezés, amely a függvényt tartalmazza. A különbség abban rejlik, hogy míg a függvénydeklarációknál a függvénynév kötelező, a függvénykifejezéseknél a név nem szükséges. A kód olvashatósága érdekében azonban célszerű a függvényeknél minden esetben nevet adni.

A függvénykifejezések egyik előnye, hogy lehetővé teszik számunkra, hogy ott hozzunk létre függvényeket, ahol szükség van rájuk, így a kódunk tisztább és érthetőbb lesz. A következő példákban láthatjuk a különbséget a függvénydeklarációk és kifejezések között:

javascript
function myFunctionDeclaration() {
function innerFunction() {} } const myFunc = function() {}; myFunc(function() { return function() {}; });

A fenti példában látható, hogy a függvénykifejezés mindig más kódszerkezetek részeként szerepel, például változó deklarációkban vagy más függvények argumentumaiként.

A függvénykifejezések különlegessége, hogy használhatjuk őket azonnal, miután definiáltuk őket, és nem szükséges őket külön hivatkozni. Ilyen típusú kifejezés az "azonnali függvénykifejezés" (IIFE, Immediate Invoked Function Expression). A következő példában láthatjuk, hogyan használhatunk egy IIFE-t:

javascript
(function() {
// valami művelet })();

Ez a konstrukció lehetővé teszi, hogy a függvényt azonnal meghívjuk a deklarálás után, anélkül, hogy külön változót vagy függvénynevet rendelünk hozzá.

A függvénykifejezéseknél gyakran találkozunk olyan szintaktikai trükkökkel, mint a +, -, ! vagy ~ operátorok, amelyek segítenek a JavaScript motorjának jelezni, hogy itt nem egy egyszerű függvény deklarációval van dolgunk, hanem egy kifejezéssel. Például:

javascript
+function(){}();
-function(){}(); !function(){}(); ~function(){}();

Ezek a kódminták mind ugyanazt az eredményt érik el, de különböző szintaktikai eszközöket használnak annak biztosítására, hogy a JavaScript megfelelően értelmezze azokat függvénykifejezésként.

A függvények JavaScript-ben tehát rendkívül rugalmasak és erőteljesek. A függvénydeklarációk és kifejezések közötti különbségek megértése alapvető fontosságú, ha hatékonyan szeretnénk használni a nyelvet, és tisztában kell lennünk azokkal a szintaktikai szabályokkal, amelyek befolyásolják a kód működését.

Hogyan kezeli a JavaScript a változókat és a függvényeket a futási környezetben?

A JavaScript nyelv egyik különleges és sokszor félreértett tulajdonsága, hogy miként kezeli a változókat és függvényeket a különböző futási környezetekben. A futási hibák elkerülésére gyakran használhatjuk az ESLint no-use-before-define szabályát, amely biztosítja, hogy a változók mindig a felhasználásuk előtt legyenek deklarálva. Azonban a dolog igazán izgalmasá válik, amikor a függvények deklarációjáról beszélünk.

A függvények deklarációja, amelyet az function functionName szintaxis határoz meg, egy olyan sajátossággal bír, hogy mindig úgy kezelik őket, mintha azok a kód elején lettek volna deklarálva. Ez azt jelenti, hogy a függvényeket bárhol elérhetjük ugyanazon a szinten belül, még akkor is, ha azok később kerülnek definiálásra. Ezt a jelenséget hoistingnak nevezzük. A hoisting olyan viselkedést eredményez, amely ellentmond a futás logikai sorrendjének, és gyakran zavaróan hat azokra, akik nem ismerik ezt a működést.

Például, ha a következő kódot futtatjuk:

javascript
greetNinja();
function greetNinja() { console.log(getGreeting()); } function getGreeting() { return "Ossu!"; }

A kimenet a következő lesz: Ossu! Itt a greetNinja() függvényhívás sikeresen lefut, még akkor is, hogy a hívás előtt nem került deklarálásra a függvény. A hoisting tulajdonságának köszönhetően a JavaScript motor úgy kezeli a kódot, mintha a függvények deklarációja már a kód elején ott lett volna, miközben a tényleges végrehajtásuk később történik.

Ez a viselkedés csak a függvénydeklarációkra érvényes. Más típusú függvények, például a függvénykifejezések, nem rendelkeznek ilyen furcsa tulajdonsággal. Továbbá, a hoisting nem csak a függvényekre vonatkozik, hanem más JavaScript konstrukciókra is, például az importálásra, amennyiben szigorú módban dolgozunk. Az importálás szintén hoisting hatása alá esik, és ebben a témában többet megtudhatunk a 13. fejezetben.

A következő fontos rész, amit érdemes megérteni, hogy hogyan működik a JavaScript motor, amikor a kódot értelmezi és végrehajtja. A JavaScript motor két fázisban dolgozza fel a kódot: először a szintaktikai ellenőrzést végzi el, hogy észlelje az esetleges hibákat, majd a teljes kódot bytekóddá alakítja. A szintaktikai ellenőrzés során a JavaScript motor már előre felismeri a szintaktikai hibákat, még mielőtt a kód végrehajtásra kerülne. Ez az ellenőrzés gyors, de a teljes elemzés már lassabb, mivel ilyenkor építi fel a változók és függvények hatókörét.

Egy egyszerű példát nézve:

javascript
console.log("func is not defined yet."); const func = () => { let a = 1; const a = 2; };

Ez a kód szintaktikai hibát eredményez, mivel ugyanazt a változónevet próbáljuk kétszer deklarálni ugyanabban a hatókörben. A JavaScript motor a szintaktikai ellenőrzés során észleli ezt, és nem engedi végrehajtani a kódot. Ezen kívül, a szintaktikai ellenőrzés sebessége sokkal gyorsabb, mint a teljes kódfeldolgozás, ami segít csökkenteni a futási idő kezdeti késését.

A JavaScript motorok gyakran alkalmaznak optimalizációs technikákat is, hogy javítsák a teljesítményt, például a "lazy parsing" vagy késleltetett elemzést. Ezen technika lényege, hogy a kódot nem elemzik teljesen azonnal, hanem a végrehajtás elindítása után fokozatosan dolgoznak fel egyes részeket, így gyorsabbá válik a program indítása, különösen nagyobb programok esetén. Ez a megközelítés különösen hasznos lehet olyan esetekben, amikor egy nagy függvény végrehajtása jelentős időt venne igénybe.

Például egy nagy JavaScript CLI alkalmazás esetén:

javascript
import * as readlinePromises from "node:readline/promises"; import { stdin, stdout } from "node:process"; const rl = readlinePromises.createInterface({ input: stdin, output: stdout, }); while (true) {
const input = await rl.question('Enter command or ".exit":\n> ');
switch (input) { case ".exit": process.exit(0); // Sok más parancs kezelése... } }

A kód inicializálásakor a JavaScript motor az összes kódot először ellenőrzi és elemzi. Ha ezt a logikát egy külön függvénybe helyezzük, akkor a program gyorsabban indul el, mivel a motor nem szükséges minden egyes parancsot azonnal feldolgoznia. Az eseményhurok inaktív állapotában a háttérben is folytatódhat a kód teljes elemzése, ami gyorsítja az alkalmazás válaszidejét.

Amikor a programban globális változókat hozunk létre, fontos figyelembe venni, hogy a JavaScript korábbi verzióiban könnyen létrejöhettek nem szándékos globális változók, ha nem használunk megfelelő deklarációs kulcsszavakat, mint a var, let vagy const. Ma már a szigorú mód biztosítja, hogy ne lehessen olyan változót hozzárendelni, amely nincs előzetesen deklarálva. Azonban, ha globális változót kell létrehoznunk, akkor azokat közvetlenül a globalThis objektumhoz is rendelhetjük, így azok bárhonnan elérhetők lesznek a programban.

Ezek az alapvető működési mechanizmusok fontos szerepet játszanak a JavaScript teljesítményében és viselkedésében. Az optimalizációk, mint a "lazy parsing", a hoisting, valamint a globális objektum kezelésének megértése segít abban, hogy hatékonyabban dolgozhassunk a nyelvvel és elkerüljük a váratlan futási hibákat.

Hogyan működnek a Promise metódusok és mikor használjuk őket a JavaScript-ben?

A Promise objektumok az aszinkron műveletek kezelésére szolgálnak, és a JavaScript egyik legfontosabb eszközévé váltak. Az alapvető fogalmakat már bemutattuk, de most nézzük meg részletesebben a Promise néhány érdekesebb metódusát és azokat az eseteket, amikor különösen hasznosak.

A Promise.any egy olyan metódus, amely egy olyan ígéretet ad vissza, amely akkor teljesül, amikor az első Promise teljesül. Ha minden Promise visszautasítja a teljesítést, akkor a visszaadott Promise elutasításra kerül, és egy tömböt ad vissza, amely tartalmazza az összes elutasítási okot. Ezzel szemben a Promise.race egy olyan ígéretet ad vissza, amely akkor teljesül, amikor az első Promise bármilyen állapotot elér, akár teljesül, akár elutasításra kerül. A Promise.any bár újabb és ritkábban használt, rendkívül hasznos lehet olyan helyzetekben, amikor többféle módon szeretnénk egy feladatot végrehajtani. Például, ha egy adatbázishoz és egy cache-hez egyszerre küldünk fetch kéréseket. Ha a cache válaszol, akkor tökéletes, ha nem, az sem gond, amíg az adatbázis válasza nem utasítja el.

A Promise.race viszont gyakran arra szolgál, hogy időkorlátot szabjunk egy másik ígéretnek. Az alábbi kód például egy időtúllépés implementálására szolgál, amely a fetch kérésre egy másik Promise-t helyez el, és a gyorsabb eredményt adja vissza:

javascript
const fetchController = new AbortController(); const fetchPromise = fetch("https://httpbin.org/delay/2", { signal: fetchController.signal, }).then((response) => { if (!response.ok) {
throw new Error(`Status code ${response.status}`);
}
return response.json(); }); const timeoutPromise = new Promise((_, reject) => setTimeout(() => { fetchController.abort();
reject(new Error("Request timeout"));
},
1_000), ); Promise.race([fetchPromise, timeoutPromise]) .then((result) => { console.log(result); }) .catch((reason) => { console.error(`Request failed: ${reason}`); });

Ebben a példában az AbortController lehetővé teszi az aszinkron műveletek leállítását, amelyet az ES2018-ban vezettek be. Az AbortController egy rendkívül hasznos API, mivel nélküle a fetch kérés a háttérben folytatódna, miután az időkorlát lejárt, ezzel fölöslegesen felhasználva a rendszer erőforrásait.

Azonban a Promise metódusok, mint például a Promise.any és a Promise.race, rendelkeznek egy fontos korláttal. Mivel ezek csak egyetlen Promise-t adnak vissza, nem alkalmasak olyan helyzetek kezelésére, amikor több, egyszerre zajló aszinkron műveletet kell koordinálni. Például, ha több Web Worker-t szeretnénk egyszerre elindítani, hogy különböző feladatokat hajtsanak végre, de egyszerre túl sok munkát indítani nem praktikus, mert túl sok memóriát használhatunk fel. Erre kínál megoldást a p-limit nevű népszerű könyvtár, amely lehetővé teszi a párhuzamos feladatok számának korlátozását.

A Promise-ok időzítésének megértése is fontos ahhoz, hogy helyesen alkalmazzuk őket. A JavaScript event loop-ja, amely egyetlen szálon működik, soha nem szakítja meg a kód végrehajtását, hanem az eseményeket sorba rendezi és azokat feldolgozza. Az események feldolgozása kétféle sorba történik: a mikrotask sorba és a makrotask sorba. A mikrotask sorba azok az események kerülnek, amelyek a Promise-ok then metódusaitól származnak. Ezzel szemben a makrotask sorba tartoznak a setTimeout vagy a DOM események, amelyek késleltetve kerülnek végrehajtásra.

Ez a prioritásrendszer lehetővé teszi, hogy a Promise visszahívása mindig hamarabb végrehajtódjon, mint a setTimeout-ok, még akkor is, ha azok nulla milliszekundumos késleltetést kaptak. Az események feldolgozásának sorrendje a következőképpen működik:

  1. A JavaScript motor végrehajtja a kódot.

  2. Ha van valami a mikrotask sorban, az első mikrotask végrehajtódik.

  3. Ha nincs mikrotask, a makrotask sorban lévő események kerülnek feldolgozásra.

Ez az eseménykezelési mechanizmus alapvetően megváltoztatja a kódot, és sokkal könnyebbé teszi annak előrejelzését. Mivel a Promise-ok mikrotask-ként kerülnek végrehajtásra, a then metódus nem hívódik meg szinkron módon, hanem mindig aszinkron módon, garantálva ezzel, hogy a Promise-on belüli kód mindig a következő szintű esemény előtt fusson le.

Ezen kívül, az ECMAScript 2015 óta a Promise-ok pontos időzítése és a mikrotask kezelés is meghatározottá vált. Az ECMAScript szabvány nem definiálja a setTimeout vagy queueMicrotask időzítését, de az HTML Living Standard biztosítja, hogy a JavaScript motorok követni fogják a pontos időzítési szabályokat.

Így a Promise-ok hatékonyan segítenek az aszinkron műveletek kezelésében, és biztosítják a kód olvashatóságát és előrejelezhetőségét. Az await kulcsszó egy egyszerűsített szintaxis formájában hozza el a Promise-ok erejét, és a then visszahívásait egyetlen sorban történő szinkron kódokként oldja meg.

Miért fontos elkerülni az "any" típus használatát TypeScript-ben?

A TypeScript egyik alapvető ereje a típusellenőrzés, amely segít a hibák korai felismerésében és megelőzésében a kód fejlesztése során. Azonban a "any" típus, amelyet akkor használunk, ha a típus nem ismert vagy nem specifikált, gyengítheti a típusellenőrzés hatékonyságát. Bár hasznos lehet a meglévő JavaScript kód migrálásakor, a TypeScript-hez való teljes körű átállás során inkább el kell kerülni.

Az "any" típus alkalmazásakor a típusellenőrzés gyakorlatilag ki van kapcsolva, ami azt jelenti, hogy bárminek átadhatunk bármit, és a rendszer nem fog figyelmeztetni, ha hibás típusú adatot próbálunk felhasználni. Egy példát véve, az alábbi kódban:

typescript
const pluralize = (str, count) => (count === 1 ? str : `${str}s`);

A változókat, mint a str és count, nem adtuk meg típusokkal, ezért a TypeScript típusellenőrzője nem tudja megállapítani, milyen típusú értékek lehetnek, így visszaesik az "any" típusra. Ebben az esetben a rendszer nem hajt végre semmilyen típusellenőrzést, és nem jelez hibát, ha a str nem egy string, vagy ha a count nem egy szám.

A legjobb gyakorlat, hogy explicit módon megadjuk a típusokat a paramétereknél, így:

typescript
const pluralize = (str: string, count: number) => count === 1 ? str : `${str}s`;

Ez lehetővé teszi a típusellenőrzés működését, és ha a függvényen belüli vagy azon kívüli kódban nem megfelelő típusú adatot adunk át, a TypeScript hibaüzenetet generál. A típus annotációk emellett biztosítják, hogy a pluralize függvény mindig egy string értéket adjon vissza.

Alternatív megoldás, hogy a függvény típusát is annotá