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.