A JavaScript mélységei: Mi micsoda?
Volt itt költözés és vizsgaidőszak, sőt még egy újabb Trónok harca évad is a végéhez ért, de a JavaScript mélységei sorozat még bőven tartogat izgalmakat, úgyhogy ugyan egy kis késéssel, de következzen a harmadik epizód.
A mai indító kérdésünk a következő: Mit csinál az alábbi kód?
function csinald() {
console.log(arguments.join(','));
}
csinald(1, 2, 3, 4);
De as always, kezdjük valahol máshol...
Talán emlékeztek még, hogy az első részben felsoroltam a típusokat és arra jutottunk, hogy a JS típusrendszere meglehetősen egyszerű. Nincsenek például külön típusok az egész- és a törtszámokra, azonban afölött az apróság fölött elsiklottunk, hogy hiányzik a tömb.
Illetve mi a helyzet az alábbi sorral?
var csinald = function() { }; //Függvény TÍPUSÚ változó?
Szóval úgy látszik, hogy kicsit figyelmetlen voltam ( ;) ) és ezt azt kihagytam, de nem baj, most pontosítom a listát...
Typeof
A typeof operátor az úgymond hivatalos mód, annak eldöntésére, hogy egy érték milyen típusú. Nézzünk néhány példát:
console.log(typeof undefined); //undefined
console.log(typeof 42); //number
console.log(typeof NaN); //number - Miért? Mert. (*)
console.log(typeof true); //boolean
console.log(typeof "valami"); //string
console.log(typeof {}); //object
console.log(typeof null); //object !!!
console.log(typeof (function() {})); //function
(*) Előretekintés: A Number típus, mint halmaz egyik lehetséges értéke a NaN. Ezt akár logikának is nevezhetjük, de ettől még azt jelenti, hogy NEM szám...
Itt három érdekes dolgot láthatunk: 1) Az undefined valóban típus, 2) a typeof nem tekinti külön típusnak a nullt (mivel az lényegében egy Null Pointer - (*) ettől még inkozistens a specifikáció) és 3) úgy látszik, hogy a függvény is típus. De akkor miért hagytam le a listáról?
Függvények
Láttunk már függvény pointert, meg C#-ban ott a delegate, szóval annyira nem szokatlan, hogy a függvény is típus. A gond csak az, hogy a függvényt nem figyelmetlenségből hagytam ki, hanem egyszerűen azért, mert nem típus. Ugyanis a függvények szimpla objektumok. (Egész pontosan olyan objektumok, amik rendelkeznek a [[Call]] interpreter szintű tulajdonsággal.)
Aki nem hiszi járjon utána, például próbáljon meg beleírni egy mezőt:
csinald.valami = 42;
Egyébként ha belegondolunk, hogy itt a függvény helyettesíti az osztályt, akkor teljesen logikus, hogy objektum legyen, hiszen ez az egyetlen módszer a statikus tagváltozok és metódusok megvalósítására.
Arra viszont, hogy a typeof miért különbözteti meg a többi objektumtól sajnos nem ad indoklást se a specifikáció, se a logika. (*) Valószínűleg, azért tekinti típusnak a typeof, mert a függvényeknek van egy saját operátora)
Tömbök
A tömbökkel kapcsolatban még izgalmasabb a helyzet. Ha megnézzük, hogy mit mond a typeof, akkor láthatjuk, hogy a tömb is objektum és a specifikáció sem tekinti külön típusnak. Ugyanakkor létezik az Array konstruktor, illetve van egy Array.isArray metódus is, ami képes eldönteni egy objektumról, hogy az tömb-e.
var tomb = []; //new Array()
console.log(typeof tomb); //object !!!
console.log(Array.isArray(tomb)) //true
console.log(Array.isArray({})) //false
És ha ez nem lenne elég izgalmas, akkor még az a furcsaság is itt van, hogy a mezei objektumok is indexelhetőek:
var obj = {};
obj[0] = "valami";
obj[1] = 42;
console.log(obj[1]); //42
Viszont hossza (length) csak a tömböknek van (meg persze a függvényeknek - csak, hogy egyszerű legyen). Illetve ott vannak a csak tömbökön definiált műveletek (push, pop, join, reverse, map...). Azaz van annyi lényeges különbség, hogy a typeof akár jelezhetné is.
Összesítve az eddigieket: úgy látszik, hogy eredetileg is pontos voltam, mert a specifikáció szerint sem a tömb, sem a függvény nem számít típusnak, viszont a typeof operátor viselkedése teljesen logikátlan.
(*) Visszatekintés: Megpróbálhatnánk beleerőltetni valami logikát, de mindenképp sántítana. Túl sok sebből vérzik a typeof, ezért bármennyire szeretném, ezúttal nem tudom leírni, hogy miért specifikálták így.
Most pedig térjünk vissza az eredeti kérdéshez és lássuk, hogy ennek az egésznek mi köze a mai feladványhoz.
Argumentumok
Egy függvény paramétereit elérhetjük az arguments nevű tömbön keresztül is, a megfelelő paraméter sorszámával indexelve azt (pl. arguments[0]). A join pedig, a paraméterként kapott szöveget használva elválasztóként, egyesíti a tömb elemeit egy sztringként. Szóval a cikk elején látható kódnak ki kellene írnia a konzolra, hogy: "1,2,3,4". De nem fogja, inkább dob egy szép hibát.
Mert sajnos az arguments nem tömb. De nem is mezei objektum.
Ha legjobb barátunkat a typeof operátort kérdezzük, akkor azt kapjuk, hogy "object", persze ettől még lehetne tömb. Ha megnézzük, hogy mit tud, akkor látható, hogy indexelhető és van hossza, még mindig stimmel a tömb. Ellenben az Array egyetlen műveletét sem ismeri, na itt már baj van. Ő egy array-like, legalábbis így szoktak rá hivatkozni a hivatalos dokumentációkban. A specifikáció szerint viszont a típusa Object és az osztálya Arguments.
[[Class]]
A JavaScriptben nincsenek osztályok, de létezik egy [[Class]] nevű interpreter szintű tulajdonság, amit direkt módon sem elérni, sem módosítani nem tudunk, viszont minden natív (azaz a specfikáció által teljes mértékben definiált - tehát nem az aktuális implementáció sajátja, mint mondjuk a Window, ami csak böngészőkben létezik) objektumhoz tartozik egy.
Ezek a jelzők egyértelműen utalnak az adott objektum céljára és működésére, úgyhogy kényelmesebb lenne ezeket használni típusként:
- Arguments: Az arguments típusa, azaz az array-like
- Array: Tömbök
- Boolean: Logikai értékek
- Date: Dátumok és időpontok
- Error: Hibák
- Function: Függvények (és konstruktorok)
- JSON: JavaScript Object Notation típuső értékek - csak az interpreterben, JS-ben sima Object
- Math: A kivétel, a matematikai műveleteket tartalmazó statikus osztály
- Number: Számok
- Object: Objektumok, beleértve az általunk létrehozottakat is
- RegExp: Reguláris kifejezések
- String: Szövegek
- Undefined: Az undefined érték
- Null: A null érték
A specifikáció szintjén ezek nagyon átfednek, ezért nem lehetnek ezek a valós típusok, viszont, ha szeretnénk egy trükkel használhatjuk őket:
function typeOf(obj) {
var type = Object.prototype.toString.call(obj);
return type.replace("[object ", "").replace("]", "").toLowerCase();
}
A trükk lényege, hogy az Object típus eredeti toString metódusa a specifikáció szerint visszaadja a [[Class]] értékét, így ebből némi formázással megkaphatjuk a typeof-nak megfelelő alakú értékeket. Ahhoz, hogy hozzáférjünk az eredetihez, szükségünk van a prototype-ra, ami minden objektum közös része. Továbbá a call is szükséges, hogy a metódusunk az aktuális objektumon hajtódjon végre. (Erről még lesz szó részletesebben.)
Két apró szépsághibája van ennek a módszernek: 1) a Math esetében a mi szintünkön logikusabb lenne "object"-et visszadni, 2) a RegExp csak az interpreter szintjén ismerhető fel, JS-ben "object"-et kapunk. Javítsuk ki ezeket gyorsan:
function typeOf(obj) {
if (obj instanceof RegExp) return "regexp";
else if(obj === Math) return "object";
else {
var t = Object.prototype.toString.call(obj);
return t.replace("[object ", "").replace("]", "").toLowerCase();
}
}
Így megszületett a logikusan működő typeOf és most már tudjuk, hogy mi micsoda. :)
Zárásként pedig javítsuk ki az eredeti kódunkat:
function csinald() {
console.log(Array.prototype.slice.call(arguments, 0).join(','));
}
csinald(1, 2, 3, 4);
Egyéb megoldások itt.
Ehhez magát az Array-t hívjuk segítségül, az előző példához hasonlóan, a prototype segítségével. Nekünk most egy olyan művelet kell, ami nem csinál semmit, erre pedig a slice a legalkalmasabb, mivel 0-ával hívva az első nulla elemet levágja és a maradékot, azaz a teljes tömböt adja vissza.
A trükk pedig, amitől ez működik, az, hogy 1) a tömb műveletek nem használják egymást (ezért array-like-on is működnek), 2) a slice létrehoz egy új tömböt és abban helyezi el a levágott részt, ezáltal megváltoztatva a típust.
Mára ennyi volt, remélem tetszett.