Bevezető

Bevezető

A JavaScript Garden egy folytonosan növekvő kódgyűjtemény amely a nyelv kihívást okozó részeit hivatott tisztázni. Itt tanácsokat kaphatsz afelől, hogyan kerüld el a leggyakoribb programozási- valamint nyelvi hibákat, hogyan írj gyorsabb kódot, és mik a legsűrűbben elkövetett bad practicek amelyekkel egy átlagos JavaScript programozó (nem) mindennapi útján találkozhat a nyelv megismerése közben.

A JavaScript Gardennek nem célja hogy megtanítsa a JavaScript nyelvet! Az itt felsorolt témák megértéséhez mindenképp szükséges némi tapasztalat. Ha a nyelv alapjait szeretnéd elsajátítani, először nézd át ezt a kiváló tutorialt a Mozilla Developer Networkön.

Szerzők

Ez a leírás két kiváló Stack Overflow felhasználó, Ivo Wetzel (szerző) és Zhang Yi Jiang (Design) tollából született.

A dokumentumot jelenleg Tim Ruffles gondozza.

Hozzájárultak még

Hosting

A JavaScript Garden a GitHubon van hostolva, de a Cramer Development jóvoltából a JavaScriptGarden.info címen is elérhető.

Licensz

A JavaScript Garden az MIT licensszel van ellátva és a GitHubon hostoljuk. Ha bármilyen hibát vagy elírást veszel észre, kérlek jelezd azt, vagy javítsd és küldj egy pull requestet. Továbbá, megtalálhatsz minket a JavasScript szobában a Stack Overflow chaten.

Objektumok

Objektumok és mezők használata

A JavaSciprtben minden objektumként működik, a null és az undefined kivételével.

false.toString(); // 'hamis'
[1, 2, 3].toString(); // '1,2,3'

function Foo(){}
Foo.bar = 1;
Foo.bar; // 1

Gyakori tévhitként terjed, hogy a JavaScriptben a számok nem használhatóak objektumként. Ez csak látszólag igaz, mivel a JavaScript a pont utáni részt úgy próbálja értelmezni, mintha lebegőpontos számot látna. Így hibát kaphatunk.

2.toString(); // SyntaxErrort vált ki

Azonban számos kifejezés létezik megoldásként, amelyekkel megkerülhető ez a probléma.

2..toString(); // így a második pont már az objektumra utal
2 .toString(); // fontos a space-t észrevenni itt a pont előtt
(2).toString(); // a 2 értékelődik ki hamarabb

Objektumok mint adattípusok

Az objektumok JavaScriptben Hash táblaként is használhatóak, mivel természetszerűleg kulcs-érték párokat tartalmaznak.

Az objektum literál leírásával - {} jelöléssel - lehet létrehozni egy új objektumot. Ez az új objektum az Object.prototype-ból származik és nincsenek saját mezői definiálva.

var foo = {}; // egy új, üres objektum

// egy új objektum egy 'test' nevű mezővel, aminek 12 az értéke
var bar = {test: 12}; 

Mezők elérése

Egy objektum mezői kétféle módon érhetőek el, vagy az 'objektum.mezőnév' jelöléssel, (Ford.: amit "dot notationként" emlegetünk) vagy a szögletes zárójelek kirakásával.

var foo = {name: 'macska'}
foo.name; // macska
foo['name']; // macska

var get = 'name';
foo[get]; // macska

foo.1234; // SyntaxError
foo['1234']; // működik

A két jelölés majdnem egyenértékűen használható, kivéve, hogy a szögletes zárójelekkel dinamkusan állíthatunk be mezőket és olyan neveket is választhatunk, amik amúgy szintaxis hibához vezetnének (Fordító: mivel a neveket stringbe kell rakni, megadhatunk a JS által "lefoglalt" kulcsszavakat is mezőnévként, habár ennek használata erősen kerülendő).

Mezők törlése

Egyetlen módon lehet mezőt törölni egy objektumból ez pedig a delete operátor használata; a mező értékének undefined-ra vagy null-ra való állítása csak magára az értékre van kihatással, de a kulcs ugyanúgy megmarad az objektumban.

var obj = {
    bar: 1,
    foo: 2,
    baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;

for(var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i, '' + obj[i]);
    }
}

A fenti ciklus a bar undefined és a foo null eredményeket fogja kiírni - egyedül a baz mező került törlésre, és emiatt hiányzik is az outputról.

Kulcsok jelölése

var test = {
    'case': 'Kulcsszó vagyok, ezért stringként kell leírnod',
    delete: 'Én is az vagyok' // SyntaxError
};

Az objektumok mezőnevei mind stringként, mind egyszerű szövegként (Ford.: aposztrófok nélkül) leírhatóak. A JavaScript értelmező hibája miatt, a fenti kód azonban SyntaxErrort eredményez ECMAScript 5 előtti verzió esetén.

Ez a hiba onnan ered, hogy a delete egy kulcsszó, viszont érdemes string literálként leírni hogy helyesen megértsék a régebbi JavaScript motorok is.

A Prototípus

A JavaScript nem a klasszikus öröklődést használja, hanem egy ún. prototípusos származtatást használ.

Míg ezt gyakran a JavaScript legnagyobb hibái között tartják számon, valójában ez a származtatási modell jóval kifejezőbb mint klasszikus barátja. Ezt jelzi, hogy például sokkal könnyebb megépíteni a klasszikus modellt, alapul véve a prototípusos modellt, míg a fordított irány kivitelezése igencsak nehézkes lenne.

A JavaScript az egyetlen széles körben elterjedt nyelv, amely ezt a származtatást használja, így mindenképp időt kell szánni a két modell közti különbség megértésére.

Az első feltűnő különbség, hogy ez a fajta származtatás prototípus láncokat használ.

function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};

function Bar() {}

// Beállítjuk a Bar prototípusát a Foo egy új példányára
Bar.prototype = new Foo(); // !
Bar.prototype.foo = 'Hello World';

// Beállítjuk a Bar konstruktorát
Bar.prototype.constructor = Bar;

var test = new Bar(); // új Bar példány létrehozása

// A kapott prototípus lánc
test [instance of Bar]
    Bar.prototype [instance of Foo]
        { foo: 'Hello World', value: 42 }
        Foo.prototype
            { method: ... }
            Object.prototype
                { toString: ... /* stb. */ }

A fenti kódban a test objektum mind a Bar.prototype és Foo.prototype prototípusokból származik, így lesz hozzáférése a method nevű függvényhez amely a Foo prototípusában lett definiálva. A value mezőhöz szintén lesz hozzáférése, amely akkor jött létre, amikor (szám szerint) egy új Foo példányt hoztunk létre. Érdemes észrevenni hogy a new Bar() kifejezés nem hoz létre egy új Foo példányt minden alkalommal, azonban újrahasználja azt az egyetlen (//!) inicilalizált Foo pédlányunkat. Így az összes Bar példány egy és ugyanazt a value mezőt (és értéket) fogja használni.

Mezők keresése

Amikor olyan utasítást adunk ki, amellyel egy objektum mezőjét keressük, a JavaScript felfele bejárja az egész prototípus láncot, amíg meg nem találja a kért mezőt.

Hogyha eléri a lánc legtetejét - nevezetesen az Object.prototype-t és még ekkor sem találja a kért mezőt, akkor az undefined-dal fog visszatérni.

A Prototype mező

Alapjáraton, a JavaScript a prototype nevű mezőt használja a prototípus láncok kialakításához, de ettől függetlenül ez is ugyanolyan mező mint a többi, és bármilyen értéket belehet neki állítani. Viszont a primitív típusokat egyszerűen figyelmen kívül fogja hagyni a feldolgozó.

function Foo() {}
Foo.prototype = 1; // nincs hatása

Az objektumok megadása, mint azt a fentebbi példában láthattuk, hatással van a prototype mezőkre és ezeknek az átállításával bele lehet szólni a prototípus láncok kialakításába.

Teljesítmény

Értelemszerűen, minnél nagyobb a prototípus lánc, annál tovább tart egy-egy mező felkeresése, és ez rossz hatással lehet a kód teljesítményére. Emellett, ha egy olyan mezőt próbálunk elérni amely nincs az adott objektum példányban, az mindig a teljes lánc bejárását fogja eredményezni.

Vigyázat! Akkor is bejárjuk a teljes láncot, amikor egy objektum mezőin próbálunk iterálni.

Natív prototípusok bővítése

Egy gyakran elkövetett hiba, hogy az Object.prototype prototípust vagy egy másik előre definiált prototípust próbálunk kiegészíteni új kóddal.

Ezt monkey patching-nek is hívják, és aktívan kerülendő, mivel megtöri az egységbe zárás elvét. Habár ezt a technikát olyan népszerű framework-ök is használják mint a Prototype, ettől függetlenül ne hagyjuk magunkat csőbe húzni; nincs ésszerű indok arra, hogy összezavarjuk a beépített típusokat, további nem standard saját funkcionalitással.

Az egyetlen ésszerű használati indok a natív prototípusokba nyúlásra az lehet, hogy megpróbáljuk szimulálni az új JavaScript motorok szolgáltatásait régebbi társaikon, például az Array.forEach implementálásával.

Zárásként

Nagyon fontos megérteni a prototípusos származtatási modellt, mielőtt olyan kódot próbálnánk írni, amely megpróbálja kihasználni a sajátosságait. Nagyon oda kell figyelni a prototípuslánc hosszára - osszuk fel több kis láncra ha szükséges - hogy elkerüljük a teljesítmény problémákat. Továbbá, a natív prototípusokat soha ne egészítsük ki, egészen addig amíg nem akarunk JavaScript motorok közötti kompatibilitási problémákat áthidalni.

hasOwnProperty

Hogy megtudjuk nézni egy adott objektum saját mezőit - azokat a mezőket amelyek az objektumon közvetlenül vannak definiálva, és nem valahol a prototípus láncon -, a hasOwnProperty függvényt használata ajánlott, amelyet az összes objektum amúgy is örököl az Object.prototype-ból.

A hasOwnProperty függvény az egyetlen olyan dolog amelyik anélkül tudja ellenőrizni az objektum mezőit, hogy megpróbálná bejárni a prototípus láncot.

// Az Object.prototype beszennyezése
Object.prototype.bar = 1;
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // igaz

foo.hasOwnProperty('bar'); // hamis
foo.hasOwnProperty('goo'); // igaz

Hogy megértsük a fontosságát, egyedül a hasOwnProperty tudja hozni a korrekt és elvárt eredményeket mezőellenőrzés szempontjából. Egyszerűen nincs más módja annak, hogy kizárjuk a szűrésünkből azokat a mezőket amelyek nem az objektumon, hanem valahol feljebb, a prototípus láncon lettek definiálva.

A hasOwnProperty mint mező

A JavaScript persze nem védi magát a hasOwnProperty nevet, így egy jókedvű programozóban mindig megvan a lehetőség, hogy így nevezze el a saját függvényét. Ennek kikerülése érdekében ajánlott mindig a hasOwnProperty-re kívülről hivatkozni (Értsd: A hackelt -saját hasOwnPropertyvel ellátott- objektum kontextusán kívüli objektum hasOwnPropertyjét hívjuk meg).

var foo = {
    hasOwnProperty: function() {
        return false;
    },
    bar: 'Mordor itt kezdődik'
};

foo.hasOwnProperty('bar'); // mindig hamissal tér vissza

// Használhatjuk egy másik objektum hasOwnPropertyjét, 
// hogy meghívjuk a foo-n.
({}).hasOwnProperty.call(foo, 'bar'); // ez már igaz

// Szintén jó megoldás lehet közvetlenül az 
// Object prototypejából hívni ezt a függvényt.
Object.prototype.hasOwnProperty.call(foo, 'bar'); // ez is igaz

Konklúzió

A hasOwnProperty használata az egyetlen megbízható módszer annak eldöntésére, hogy egy mező közvetlenül az objektumon lett-e létrehozva. Melegen ajánlott a hasOwnProperty-t minden for in ciklusban használni. Használatával ugyanis elkerülhetjük a kontár módon kiegészített natív prototípusokból fakadó esetleges hibákat, amire példát az imént láttunk.

A for in ciklus

Csak úgy mint a jó öreg in operátor, a for in is bejárja az egész prototípus láncot, amikor egy objektum mezőin próbálnánk iterálni.

// Mérgezzük Object.prototypeot!
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
    console.log(i); // mind a moo és bar is kiírásra kerül
}

Mivel -hála égnek- magának a for in ciklusnak a működését nem lehet befolyásolni, így más módszert kell találnunk ahhoz hogy száműzzük a váratlan mezőket a ciklus magból. (Értsd: Azokat amelyek a prototípus láncon csücsülnek csak). Ezt pedig az Object.prototype-ban lakó hasOwnProperty függvény használatával érhetjük el.

Szűrés használata a hasOwnProperty-vel

// még mindig a fenti foo-nál tartunk
for(var i in foo) {
    if (foo.hasOwnProperty(i)) {
        console.log(i);
    }
}

Ez az egyetlen helyes útja annak hogy az objektum saját mezőin iteráljunk csak végig. Mivel a hasOwnProperty-t használjuk, így csak a várt moo-t fogja kiírni. Tehén jó kódunk van! Hogyha a hasOwnProperty-t kihagynánk, a kódunk ki lenne téve nem várt hibáknak, amik pl. abból fakadnak hogy valaki ocsmányul kiterjesztette az Object.prototype-t.

Például, ha a Prototype frameworköt használjuk, és nem ilyen stílusban írjuk a ciklusainkat, a hibák szinte garantáltak, ugyanis ők saját szájízükre kiterjesztik az Object.prototype-t.

Konklúzió

A hasOwnProperty használata erősen javasolt. Soha ne éljünk pozitív feltételezésekkel a futó kódot illetően, főleg olyan döntésekben nem érdemes orosz rulettezni, mint hogy kiterjeszti-e valaki a natív prototípusokat vagy nem. Mert általában igen.

Függvények

Függvény deklarációk és kifejezések

A függvények JavaScriptben egyben objektumok is. Ez azt jelenti, hogy ugyanúgy lehet őket passzolgatni mint bármelyik más értékeket. Ezt a featuret gyakran használják arra, hogy egy névtelen (callback) függvényt átadjunk egy másik -aszinkron- függvény paramétereként.

A függvény deklaráció

function foo() {}

Ez a függvény felkerül a scope tetejére (hoisting), mielőtt a kód végrehajtása megtörténne. Így abban a scopeban ahol definiálták, mindenhol elérhető, még abban a trükkös esetben is, hogyha a kód azon pontján hívjuk ezt a függvényt, mielőtt definiáltuk volna (látszólag).

foo(); // Így is működik
function foo() {}

A függvény kifejezés (expression)

var foo = function() {};

A fentebbi példában egy névtelen függvényt adunk értékül a foo változónak.

foo; // 'undefined'
foo(); // TypeError hiba
var foo = function() {};

Habár ebben a példában a var deklaráció futás előtt a kód tetejére kúszik, ettől függetlenül a foo mint függvény meghívásakor hibát fogunk kapni.

Ugyanis a deklaráció felkúszott, azonban az értékadás csak futásidőben fog megtörténni, addig is a foo változó értéke undefined marad. Az undefinedet pedig hiába hívjuk függvényként, TypeErrort kapunk végeredményül.

Névvel ellátott függvény kifejezés

Egy másik érdekes eset, amikor névvel ellátott függvényeket adunk értékül változóknak.

var foo = function bar() {
    bar(); // Működik
}
bar(); // ReferenceError

Ebben a példában a bart önmagában nem lehet elérni egy külső scopeból (utolsó sor), mivel egyből értékül adtuk a foo változónak. Ennek ellenére a baron belül elérhető a bar név. A tanulság az, hogy a függvény önmagát mindig eléri a saját scopeján belül, és ez a JavaScriptben található névfeloldásnak köszönhető.

A this mágikus működése

A this kicsit másképp működik a JavaScriptben mint ahogy azt megszokhattuk más nyelvekben. Ugyanis pontosan ötféle módja lehet annak hogy a this éppen mire utal a nyelvben.

A Globális hatókör

this;

Amikor globális hatókörben van használva a this, akkor pontosan a globális objektumra utal.

Függvény híváskor

foo();

Itt, a this megint a globális objektumra fog utalni.

Eljárás hívásakor

test.foo(); 

Ebben a példában a this a test objektumra fog hivatkozni.

Konstuktor hívásakor

new foo(); 

Ha a függvény hívását a new kulcsszóval előzzük meg, akkor a függvény konstruktorként fog viselkedni. A függvényen belül, a this az újonnan létrehozott Objektumra fog hivatkozni.

A this explicit beállítása

function foo(a, b, c) {}

var bar = {};
foo.apply(bar, [1, 2, 3]); // ugyanaz mint egy sorral lejjebb
foo.call(bar, 1, 2, 3); // argumentumok: a = 1, b = 2, c = 3

A Function.prototype-ban levő call vagy apply használatakor aztán elszabadul a pokol :). Ezekben az esetekben ugyanis a this a foo hívásakor egzaktan be lesz állítva az apply/call első argumentumára.

Ennek eredményképp az előzőekben említett Eljárás hívásakor rész nem érvényes, a foo fentebbi meghívásakor a this értéke a bar objektumra lesz beállítva.

Gyakori buktatók

Míg a fent megtalálható eseteknek van gyakorlatban vett értelme, az első a nyelv rossz designjára utal, ugyanis ennek soha nem lesz semmilyen praktikus felhasználási módja.

Foo.method = function() {
    function test() {
        // A this itt a globális ojjektum.
    }
    test();
};

Gyakori hiba, hogy úgy gondolják a fenti példában az emberek, hogy a this a test függvényen belül az őt körülvevő Foo-ra fog mutatni, pedig nem.

Megoldásképp, hogy a Foo-hoz hozzáférhessük a test-en belül, szükségszerű egy változót lokálisan elhelyezni a method-on belül, ami már valóban a kívánt this-re (Foo-ra) mutat.

Foo.method = function() {
    var that = this;
    function test() {
        // Használjuk a that-et a this helyett
    }
    test();
};

A that tuladjonképpen egy mezei változónév (nem kulcsszó), de sokszor használják arra, hogy egy másik this-re hivatkozzanak vele. A colsureökkel kombinálva ez a módszer arra is használható hogy this-eket passzolgassunk a vakvilágban és mégtovább.

Eljárások értékül adása

Egy másik koncepció ami nem fog a JavaScriptben működni, az az alias függvények létrehozása, ami tulajdonképpen egy függvény másik névhez való kötését jelentené.

var test = someObject.methodTest;
test();

Az első eset miatt a test egy sima függvényhívásként működik, azonban a this értéke a függvényen belül a továbbiakban nem a someObject lesz.

Elsőre a this alábbi módon való utánkötése (late binding) nem tűnik jó ötletnek. Azonban ez az, amitől a prototípusos öröklődés is működni tud, ami a nyelv egyik fő erőssége.

function Foo() {}
Foo.prototype.method = function() {};

function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

Amikor a method meghívódik a Bar példányaként, a this pontosan a Bar megfelelő példányára fog mutatni.

Closure-ök és referenciák

A JavaScript nyelv egyik legerőteljesebb tulajdonsága a closure-ök használatában rejlik. Ezek használatával a hatókörök egymásba ágyazhatóak, és egy belső hatókör mindig hozzáfér az őt körülvevő, külső hatókör változóihoz. Miután JavaScriptben egyetlen dologgal lehet hatóköröket kifejezni, és ez a funkció (bizony az if, try/catch és hasonló blokkok nem jelentenek új hatókört, mint pl. a Javaban), az összes funkció closure-ként szerepel.

Privát változók emulálása

function Counter(start) {
    var count = start;
    return {
        increment: function() {
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

Ebben a példában a Counter két closure-rel tér vissza: az increment és a get funkcióval. Mind a két funkció referenciát tárol a Counter hatókörre, és így mindketten hozzáférnek a count változóhoz, ami ebben a hatókörben lett definiálva.

Miért működnek a privát változók?

Mivel a JavaScriptben egyszerűen nem lehet hatókörre referálni, vagy hatókört értékül adni, így ezért szintén lehetetlen elérni az iménti count változót a külvilág számára. Egyetlen mód van a megszólítására, ezt pedig láttuk a fentebbi két closure-ön belül.

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};

A fentebbi kód nem fogja megváltoztatni a Counter hatókör count változóját, mivel a foo.hack mező nem abban a hatókörben lett létrehozva. Ehelyett, okosan, létre fogja hozni, vagy felül fogja írni a globális count változót (window.count).

Closure-ök használata ciklusokban

Az egyik leggyakoribb hiba amit el lehet követni, az a closure-ök ciklusokban való használata. Annak is azon speciális esete amikor a ciklus indexváltozóját szeretnénk lemásolni a closure-ön belül.

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

A fenti kódrészlet marhára nem a számokat fogja kiírni 0-tól 9-ig, de inkább a 10-et fogja tízszer kiírni.

Ugyanis a belső névtelen függvény egy referenciát fog tárolni a külső i változóra, és akkor, amikor végül a console.log sor lefut, a for loop már végzett az egész ciklussal, így az i értéke 10-re lesz beállítva.

Ahhoz, hogy a várt működést kapjuk (tehát a számokat 0-tól 9-ig), szükségszerű az i változó értékét lemásolni.

A referencia probléma elkerülése

Az előző problémára megoldást úgy lehet jól adni, hogy az utasításoknak megfelelően lemásoljuk a ciklusváltozót, úgy hogy a jelenlegi ciklusmagöt körbevesszük egy névtelen függvénnyel.

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

A külső (wrapper) névtelen függvény így azonnal meghívódik az i ciklusváltozóval, mint paraméterrel, és így mindig egy másolatot fog kapni az i változó értékéről, amit ő e néven emészt tovább.

Így a setTimeoutban lévő névtelen fgv. mindig az e nevű referenciára fog mutatni, aminek az értéke így már nem változik meg a ciklus futása során.

Egy másik lehetséges út a megoldáshoz az, hogy egy wrapper függvényt visszatérítünk a setTimeoutból, aminek ugyanaz lesz a hatása, mint a fentebbi példának.

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

Az arguments objektum

Minden függvényhatókörben hozzáférhető az arguments nevű speciális változó, amely azon argumentumok listáját tartalmazza, amelyekkel a függvényt meghívták.

Lehet hogy úgy néz ki, de az arguments objektum nem egy tömb. Látszólag hasonlít rá, mivel van például egy length nevű mezője, de igazából nem az Array.prototype-ból "származik", hanem tisztán az Object-ből.

Itt jön a trükk lényege, hogy ennek köszönhetően nem használhatóak rajta a standard tömb műveletek mint például a push, pop vagy a slice. Míg a sima for ciklusos iterálás működik itt is, ahhoz hogy az előbb említett műveleteket is tudjuk rajta használni, át kell konvertálni egy valódi Array objektummá.

Tömbbé konvertálás

Ez a kódrészlet egy új Array objektummá varázsolja az emlegetett arguments szamarat.

Array.prototype.slice.call(arguments);

De, ez a konverzió meglehetősen lassú így egyáltalán nem ajánlott teljesítmény kirtikus alkalmazások írásakor.

Argumentumok kezelése

A következő módszer ajánlott arra az esetre hogyha az egyik függvény paramétereit egy-az-egyben át szeretnénk adni egy másik függvény számára.

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // sok okos kód ide
}

Egy másik trükk arra hogy teljesen független wrapper függvényeket gyártsunk, a call és apply együttes használata.

function Foo() {}

Foo.prototype.method = function(a, b, c) {
    console.log(this, a, b, c);
};

// Elkészíti a "method" (this) független verzióját
// Ezeket kapja paraméterül: this, arg1, arg2...argN
Foo.method = function() {

    // Eredmény: Foo.prototype.method.call(this, arg1, ...argN)
    Function.call.apply(Foo.prototype.method, arguments);
};

Paraméterek és argumentum indexek

A háttérben az arguments objektum minden egyes indexére (elemére) egy getter és egy setter függvényt is kap, csak úgy ahogy a függvény paramétereit is felül tudjuk írni, illetve eltudjuk érni.

Ennek eredményeképp, az arguments objektumon véghezvitt változtatások szinkronban változtatják a függvény névvel ellátott paramétereit is.

function foo(a, b, c) {
    arguments[0] = 2;
    a; // 2

    b = 4;
    arguments[1]; // 4

    var d = c;
    d = 9;
    c; // 3
}
foo(1, 2, 3);

Teljesítmény mítoszok és trükkök

Ahogy már azt korábban körvonalaztuk, az arguments objektum csak akkor nem jön létre, hogyha a függvényhatókörön belül definiálunk egy változót ezzel a névvel, vagy a függvényünk egyik paraméterének ezt a nevet választjuk.

Azonban a getterek és setterek mindig létrejönnek, de ez ne zavarjon meg minket, mert semmiféle befolyása nincs a teljesítményre, pláne olyan kódban ahol sokkal több mindennel is foglalkozunk mint az arguments objetkumhoz való hozzáférés.

Habár, egyetlen eset van, amelynek komoly hatása lehet a kód teljesítményére a modern JavaScript motorokban. Ez pedig az arguments.callee használata.

function foo() {
    // ..csinálunk valamit
    arguments.callee; // ezzel a függvény objektummal
    arguments.callee.caller; // és ennek a hívójával..
}

function bigLoop() {
    for(var i = 0; i < 100000; i++) {
        foo(); // Így viszont nem lehet behelyettesíteni ide...
    }
}

A fenti kódban a foo helyére nem lehet egyszerűen behelyettesíteni a függvény törzsét, mivel a függvény törzsének fogalma kell legyen mind magáról, mind az ő hívójáról. Ez nem csak hogy azt akadályozza meg, hogy a behelyettesítéssel nyerjünk egy kis többlet performanciát, de az egységbe zárás elvét is erősen keresztbevágja, hiszen a függvény így erősen támaszkodni fog a hívó környezetére (kontextusára).

Emiatt is, az arguments.callee, vagy bármely mezőjének használata erősen kerülendő.

Konstruktorok

Csak úgy mint minden más, a konstruktorok működése szintén különbözik a megszokottól. Itt minden függvényhívás amelyet a new kulcsszó előz meg, konstruktor hívásnak számít.

A this értéke a konstruktoron - hívott függvényen - belül az újonnan létrehozott objektumra mutat. Az új objektum prototípusa a konstruktor függvény prototípusával fog megegyezni.

Ha a konstruktor függvényben nincs return utasítás, akkor automatikusan a this értékével tér vissza - a létrehozott objektummal.

function Foo() {
    this.bla = 1;
}

Foo.prototype.test = function() {
    console.log(this.bla);
};

var test = new Foo();

A fenti kódban a Foo függvényt mint konstruktort hívjuk meg, ami a test változóban egy új objektumot fog eredményezni. Ennek az objektumnak a prototípusa a Foo prototípusa lesz.

Trükkös ugyan, de ha mégis van return utasítás az éppen konstruált függvényben, akkor a függvény hívása az annak megfelelő értékkel fog visszatérni, de csak akkor, ha a visszatérített érték Objektum típusú.

function Bar() {
    return 2;
}
new Bar(); // ez egy új üres objektum lesz: {}, a 2 helyett

function Test() {
    this.value = 2;

    return {
        foo: 1
    };
}
new Test(); // ez a { foo: 1 } objektumot fogja eredményezni

Hogyha kihagyjuk a new kulcsszó használatát, a függvény nem egy új objektummal fog visszatérni.

function Foo() {
    this.bla = 1; // ez a globális objektumon állít
}
Foo(); // undefined

A this JavaScript beli működésének köszönhetően, mégha le is fut az előbbi kód, akkor a this helyére a globális objektumot képzeljük.

Gyárak (Factory-k)

Ahhoz, hogy teljesen eltudjuk hagyni a new kulcsszó használatát, a konstruktor függvény explicit értékkel kell visszatérjen.

function Bar() {
    var value = 1;
    return {
        method: function() {
            return value;
        }
    }
}
Bar.prototype = {
    foo: function() {}
};

new Bar();
Bar();

Mindkét Bar-ra történő hívásmód ugyanazt fogja eredményezni. Kapunk általuk egy újonnan létrehozott objektumot, amelynek lesz egy method nevű mezője, ami egyébiránt egy Closure.

Azt is érdekes itt megjegyezni, hogy a new Bar() hívás nem befolyásolja a visszatérített objektum prototípusát. Mivel a prototípus csak az újonnan létrehozott objektumon létezik, amit a Bar nem térít vissza (mivel egy explicit értéket ad vissza).

A fenti példában nincs funkcionális különbség aközött hogy kiírjuk-e a new varázsszót avagy nem.

Új objektumok létrehozása gyárakon keresztül

Gyakran bevett módszer egy projetkben, hogy a new varázsszó használatát teljesen elhagyjuk, mert a kiírásának elfelejtése bugokhoz vezetne.

Ennek érdekében egy új objektum létrehozásához inkább egy gyárat kell implementálni, és annak a belsejében létrehozni az új objektumot.

function Foo() {
    var obj = {};
    obj.value = 'blub';

    var private = 2;
    obj.someMethod = function(value) {
        this.value = value;
    }

    obj.getPrivate = function() {
        return private;
    }
    return obj;
}

A fenti kód ugyan ellenálló a hiányzó new kulcsszó hibáját illetően és megfelelően használ privát változókat, érdemes megemlíteni a dolgok kontra részét is.

  1. Több memóriát használ, mivel az így létrehozott objektumok nem osztják meg a prototípusukat egymás között.
  2. A származtatás macerás, mivel a gyár kénytelen ilyenkor lemásolni az összes származtatandó metódust egy másik objektumról, vagy ezt az objektumot be kell állítsa a létrehozott új objektum prototípusának.
  3. Az a megközelítés miszerint egy kifelejtett new kulcsszó miatt eldobjuk az objektum teljes prototípusát, ellenkezik a nyelv szellemiségével.

Összefoglaló

A new varázsszó kihagyása ugyan bugokhoz vezethet, de ez nem megfelelő indok arra hogy ezért eldobjuk a prototípusok használatát. Végeredményben mindig az fog dönteni a különböző stílusok megválasztása között, hogy mire van szüksége éppen az aktuális programunknak. Egy dolog azért elengedhetetlenül fontos, ez pedig hogy megválasszuk melyik stílust fogjuk használni objektumok létrehozásra, és ezt konzisztensen használjuk a teljes megoldáson keresztül.

Névterek és hatókörök

Habár látszólag a kapcsos zárójelek jelentik a blokkok határait JavaScriptben, fontos megjegyezni hogy nincsen blokk szintű hatókör, csakis függvény hatókörök léteznek.

function test() { // ez egy hatókör
    for(var i = 0; i < 10; i++) { // ez meg nem
        // utasítások...
    }
    console.log(i); // 10
}

A nyelvben nincsenek beépített névterek, ami azt jelenti hogy minden, egyetlen globálisan megosztott névtérben kerül deklarálásra.

Akárhányszor egy változóra hivatkozunk, a JavaScript elkezdi felfele utazva megkeresni hatókörökön, amíg csak meg nem találja. Hogyha elérjük a globális hatókört és még mindig nem találjuk a keresett változót, akkor egy ReferenceError hibával gazdagodik a futásidőnk.

A globális változók csapása

// A script
foo = '42';

// B script
var foo = '42'

Érdemes észrevenni, hogy a fenti két scriptnek nem ugyanaz a hatása. Az A script egy foo nevű változót vezet be a globális hatókörben, a B script pedig egy foo nevű változót deklarál az ő hatókörében.

Mégegyszer tehát, ez a kettő nem ugyanazt jelenti: a var elhagyásának jópár beláthatatlan következménye is lehet.

// globális hatókör
var foo = 42;
function test() {
    // lokális hatókör
    foo = 21;
}
test();
foo; // 21

Itt, a var elhagyása azt eredményezi, hogy a test függvény mindig felülírja a globális hatókörben definiált foo változó értékét. Habár ez elsőre nem tűnik nagy dolognak, ha a varokat több száz sornyi JavaScript kódból hagyjuk el, az olyan hibákhoz vezethet, amit még az anyósunknak se kívánnánk.

// globális hatókör
var items = [/* random lista */];
for(var i = 0; i < 10; i++) {
    subLoop();
}

function subLoop() {
    // a subLoop hatóköre
    for(i = 0; i < 10; i++) { // hiányzik a var
        // elképesztő dolgokat művelünk itt
    }
}

Ennél a kódnál a külső ciklus az első subLoop hívás után megáll, mivel a subLoop felülírja az i változó globális értékét. Hogyha a második for ciklusban használtuk volna var-t azzal könnyen elkerülhettük volna ezt a hibát. Sose hagyjuk el a var utasítást, ha csak nem direkt az a kívánt hatás, hogy befolyásoljuk a külső hatókört.

Lokális változók

Kétféleképp (és nem több módon) lehet lokális változókat JavaScriptben leírni; ez vagy a függvény paraméter vagy a var utasítás.

// globális hatókör
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
    // a test függvény lokális hatóköre
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);

Itt a foo és i lokális változók a test hatókörén belül, viszont a baros értékadás felül fogja írni a hasonló nevű globális változót.

Hoisting

A JS hoistolja (megemeli) a deklarációkat. Ez azt jelenti hogy minden var utasítás és függvény deklaráció az őt körülvevő hatókör tetejére kerül.

bar();
var bar = function() {};
var someValue = 42;

test();
function test(data) {
    if (false) {
        goo = 1;

    } else {
        var goo = 2;
    }
    for(var i = 0; i < 100; i++) {
        var e = data[i];
    }
}

A fenti kód átalakul egy másik formára mielőtt lefutna. A JavaScript felmozgatja a var utasításokat és a függvény deklarációkat, az őket körülvevő legközelebbi hatókör tetejébe.

// a var utasítások felkerülnek ide
var bar, someValue; // alapból mindegyik 'undefined' értékű lesz

// a függvény deklaráció is felkerül ide
function test(data) {
    var goo, i, e; // ezek is felkerülnek
    if (false) {
        goo = 1;

    } else {
        goo = 2;
    }
    for(i = 0; i < 100; i++) {
        e = data[i];
    }
}

bar(); // Ez TypeErrorral elszáll, mivel a bar még 'undefined'
someValue = 42; // az értékadásokat nem piszkálja a hoisting
bar = function() {};

test();

A hiányzó blokk hatókör ténye nem csak azt eredményezi, hogy a var utasítások kikerülnek a ciklusmagokból, hanem az if utasítások kimenetele is megjósolhatatlan lesz.

Habár úgy látszik az eredeti kódban, hogy az if utasítás a goo globális változót módosítja, a hoisting után látjuk hogy valójában a lokális változóra lesz befolyással. Trükkös.

A hoisting tudása nélkül valaki azt hihetné, hogy az alábbi kód egy ReferenceError -t fog eredményezni.

// nézzük meg hogy a SomeImportantThing inicializálva lett-e
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}

Persze ez működik, annak köszönhetően hogy a var utasítás a globális hatókör tetejére lett mozgatva.

var SomeImportantThing;

// más kódok még inicializálhatják az előbbi változót itt...

// ellenőrizzük hogy létezik-e
if (!SomeImportantThing) {
    SomeImportantThing = {};
}

Névfeloldási sorrend

JavaScriptben az összes hatókörnek -beleértve a globálisat is- megvan a maga this változója, amelyik mindig az aktuális objektumra utal.

A függvény hatókörökben van még egy speciális arguments változó is mindig definiálva, amely a függvénynek átadott argumentumokat tartalmazza.

Hogy hozzunk egy példát, amikor valaki a foo nevű változót próbálja elérni egy függvény hatókörön belül, a JavaScript az alábbi sorrendben fogja keresni az adott változó nevet.

  1. Abban az esetben ha találunk var foo utasítást, használjuk azt.
  2. Hogyha bármelyik függvény paraméter neve foo, használjuk azt.
  3. Hogyha magának a függvénynek a neve `foo, használjuk azt.
  4. Menjünk a külső hatókörre, és kezdjük újra #1-től.

Névterek

Hogyha egyetlen globális névterünk van, akkor egy gyakori probléma lehet az, hogy névütközésekbe futunk. A JavaScriptben szerencsére ez a gond könnyen elkerülhető a névtelen wrapper függvények használatával.

(function() {
    // egy 'öntartalmazó' névtér

    window.foo = function() {
        // egy exportált closure
    };

})(); // a függvényt azonnal végre is hajtjuk

A névtelen függvények kifejezésekként vannak értelmezve; így ahhoz hogy meghívhatóak legyenek, először ki kell értékelni őket.

( // a függvény kiértékelése a zárójeleken belül
function() {}
) // a függvény objektum visszatérítése
() // az eredmény meghívása

Persze más kifejezések is használhatóak arra hogy kiértékeljük és meghívjuk a függvény kifejezést, amelyek habár szintaxisukban eltérnek, ugyanazt eredményezik.

// Még több stílus anonymus függvények azonnali hívásához...
!function(){}()
+function(){}()
(function(){}());
// és a lista folytatódik...

Összegzésül

Az anonym wrapper függvények használata erősen ajánlott a kód egységbezárása érdekében, saját névtér alkotásához. Ez nem csak hogy megvédi a kódunkat a névütközésektől, de jobb modularizációhoz is vezet.

Emelett a globális változók használata nem ajánlott. Bármilyen fajta használata rosszul megírt kódról árulkodik, amelyik könnyen eltörik és nehezen karbantartható.

Tömbök

Tömb iteráció és tulajdonságok

Habár a tömbök a JavaScriptben objektumok, nincsen jó ok arra, hogy a for in ciklussal járjuk be őket. Valójában sokkal több jó ok van arra, hogy miért ne így tegyünk.

Mivel a for in ciklus a prototípus láncon levő összes tulajdonságon végigmegy, és mivel az egyetlen út ennek megkerülésére a hasOwnProperty használata, így majdnem hússzor lassabb mint egy sima for ciklus.

Iteráció

Annak érdekébern hogy a legjobb teljesítményt érjük el a tömbökön való iteráció során, a legjobb hogyha a klasszikus for ciklust használjuk.

var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
    console.log(list[i]);
}

Még egy érdekesség van a fenti példában, ami a tömb hosszának cachelését végzi a l = list.length kifejezés használatával.

Habár a length tulajdonság mindig magán a tömbön van definiálva, még mindig lehet egy kis teljesítmény kiesés amiatt hogy minden iterációban újra meg kell keresni ezt a tulajdonságot. Persze a legújabb JavaScript motorok talán használnak erre optimalizációt, de nem lehet biztosan megmondani hogy ahol a kódunk futni fog, az egy ilyen motor-e vagy sem.

Valójában, a cachelés kihagyása azt eredményezheti, hogy a ciklusunk csak fele olyan gyors lesz mintha a cachelős megoldást választottuk volna.

A length mező

Míg a length mező getter függvénye egyszerűen csak visszaadja a tömbben levő elemek számát, addig a setter függvény használható arra (is), hogy megcsonkítsuk a tömbünket.

var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]

foo.length = 6;
foo.push(4);
foo; // [1, 2, 3, undefined, undefined, undefined, 4]

Egy rövidebb hossz alkalmazása csonkítja a tömböt. A nagyobb hossz megadása értelemszerűen növeli.

Összegzésül

A megfelelő teljesítmény érdekében, a for ciklus használata és a length cachelése ajánlott. A for in ciklus használata a tömbökön a rosszul megírt kód jele, amely tele lehet hibákkal, és teljesítményben sem jeleskedik.

Az Array konstruktor

Mivel az Array konstruktora kétértelműen bánik a paraméterekkel, melegen ajánlott mindig a tömb literált - [] jelölés - használni új tömbök létrehozásakor.

[1, 2, 3]; // Eredmény: [1, 2, 3]
new Array(1, 2, 3); // Eredmény: [1, 2, 3]

[3]; // Eredmény: [3]
new Array(3); // Eredmény: []
new Array('3') // Eredmény: ['3']

Abban az esetben, hogyha ez a konstruktor csak egy szám paramétert kap, akkor visszatérési értékül egy olyan tömböt fog létrehozni amelynek a length mezője akkorára van beállítva, ahogy azt megadtuk az argumentumban. Megjegyzendő hogy csak a length tulajdonság lesz ekkor beállítva; az egyes indexek külön-külön nem lesznek inicializálva.

var arr = new Array(3);
arr[1]; // undefined
1 in arr; // hamis, nincs ilyen index

A tömb hosszának közvetlen állítása amúgy is csak elég kevés esetben használható értelmesen, mint például alább, hogyha el akarjuk kerülni a for ciklus használatát egy string ismétlésekor.

new Array(count + 1).join(ismetlendoString);

Összegzésül

Az Array konstruktor közvetlen használata erősen kerülendő. A literálok használata elfogadott inkább, mivel rövidebbek, tisztább a szintaxisuk és olvashatóbb kódot eredményeznek.

Típusok

Egyenlőség vizsgálat

A JavaScriptben két különböző megoldás létezik az objektumok egyenlőségének vizsgálatára

Az egyenlőség operátor

Az egyenlőség vizsgálatot végző (egyik) operátort így jelöljük: ==

A JavaScript egy gyengén típusos nyelv. Ez azt jelenti hogy az egyenlőség operátor típuskényszerítést alkalmaz ahhoz, hogy össze tudjon hasonlítani két értéket.

""           ==   "0"           // hamis
0            ==   ""            // igaz
0            ==   "0"           // igaz
false        ==   "false"       // hamis
false        ==   "0"           // igaz
false        ==   undefined     // hamis
false        ==   null          // hamis
null         ==   undefined     // igaz
" \t\r\n"    ==   0             // igaz

A fenti táblázat szépen mutatja hogy mi a típuskényszerítés eredménye, és egyben azt is, hogy miért rossz szokás a == használata. Szokás szerint, ez megint olyan fícsör ami nehezen követhető kódhoz vezethet a komplikált konverziós szabályai miatt.

Pláne, hogy a kényszerítés teljesítmény problémákhoz is vezet; ugyanis, mielőtt egy stringet egy számhoz hasonlítanánk azelőtt a karakterláncot át kell konvertálni a megfelelő típusra.

A szigorú(bb) egyenlőség operátor

Ez az operátor már három egyenlőségjelből áll: ===.

Ugyanúgy működik mint az előbbi, kivéve hogy ez a változat nem alkalmaz típuskényszerítést az operandusai között.

""           ===   "0"           // hamis
0            ===   ""            // hamis
0            ===   "0"           // hamis
false        ===   "false"       // hamis
false        ===   "0"           // hamis
false        ===   undefined     // hamis
false        ===   null          // hamis
null         ===   undefined     // hamis
" \t\r\n"    ===   0             // hamis

A felső eredmények sokkal egyértelműbbek és ennek köszönhetően sokkal hamarabb eltörik a kód egy-egy ellenőrzésen. Ettől sokkal hibatűrőbb lesz a produktumunk, és ráadásul teljesítménybeli gondjaink sem lesznek.

Objektumok összehasonlítása

Habár mind a ==-t és a ===-t is egyenlőség operátornak hívjuk, eltérően viselkednek hogyha legalább az egyik operandusuk egy objektum.

{} === {};                   // hamis
new String('foo') === 'foo'; // hamis
new Number(10) === 10;       // hamis
var foo = {};
foo === foo;                 // igaz

Ebben az esetben mindkét operátor identitást és nem egyenlőséget ellenőriz; tehát azt fogják ellenőrizni hogy az operandus két oldalán ugyanaz az objektum referencia áll-e, mint az is operátor Pythonban vagy a pointerek összehasonlítása C-ben. (A ford.: Tehát nem azt, hogy a két oldalon álló objektumnak például ugyanazok-e a mezői, hanem azt hogy ugyanazon a memóriacímen található-e a két operandus).

Összegzésül

Azt érdemes tehát megjegyezni, hogy a szigorú egyenlőség vizsgálatot érdemes mindig használni. Amikor szeretnék típuskényszerítést alkalmazni, akkor azt inkább tegyük meg direkt módon, és ne a nyelv komplikált automatikus szabályaira bízzuk magunkat.

A typeof vizsgálat

A typeof operátor (az instanceof-al karöltve) lehetőség szerint a JavaScript nyelv egyik legnagyobb buktatója, mivel majdnem teljesen rosszul működik.

Habár az instanceof-nak korlátozottan még lehet értelme, a typeof operátor tényleg csak egyetlen praktikus use case-zel rendelkezik és ez nem az hogy egy objektum típusvizsgálatát elvégezzük.

Az instanceof operátor

Az instanceof operátor a két operandusának konstruktorait hasonlítja össze. Csak akkor bizonyul hasznosnak, amikor saját készítésű objektumokon alkalmazzuk. Beépített típusokon ugyanolyan hasztalan alkalmazni mint a typeof operátort.

Saját objektumok összehasonlítása

function Foo() {}
function Bar() {}
Bar.prototype = new Foo();

new Bar() instanceof Bar; // igaz
new Bar() instanceof Foo; // igaz

// Ez csak a Bar.prototypeot beállítja a Foo fv. objektumra,
// de nem egy kimondott Foo példányra
Bar.prototype = Foo;
new Bar() instanceof Foo; // hamis

Az instanceof reakciója natív típusokra

new String('foo') instanceof String; // igaz
new String('foo') instanceof Object; // igaz

'foo' instanceof String; // hamis
'foo' instanceof Object; // hamis

Érdemes itt megjegyezni hogy az instanceof nem működik olyan objektumokon, amelyek különböző JavaScript kontextusokból származnak (pl. különböző dokumentumok a böngészőn belül), mivel a konstruktoruk nem pontosan ugyanaz az objektum lesz.

Összegzésül

Az instanceof-ot tehát csak megegyező JS kontextusból származó, saját készítésű objektumoknál használjuk. Minden más felhasználása kerülendő, csak úgy mint a typeof operátor esetén.

Típus kasztolás

Előre kössük le, hogy a JavaScript egy gyengén típusos nyelv, így ahol csak tud, ott típus kényszerítést használ.

// Ezek igazak
new Number(10) == 10; // A Number.toString() számmá lesz
                      // visszaalakítva

10 == '10';           // A Stringek visszaalakulnak számmá
10 == '+10 ';         // Mégtöbb string varázslat
10 == '010';          // és mégtöbb
isNaN(null) == false; // a null varázslatosan 0-vá alakul
                      // ami persze nem NaN

// Ezek hamisak
10 == 010;
10 == '-10';

Hogy elkerüljük a fenti varázslatokat, a szigorú egyenlőség ellenőrzés melegen ajánlott. Habár ezzel elkerüljük a problémák farkasrészét, még mindig tartogat a JS gyengén típusos rendszere meglepetéseket.

Natív típusok konstruktorai

A jó hír az, hogy a natív típusok mint a Number és a String különféle módon viselkednek hogyha a new kulcsszóval avagy anélkül vannak inicializálva.

new Number(10) === 10;     // Hamis, Objektum vs. Szám
Number(10) === 10;         // Igaz, Szám vs. szám
new Number(10) + 0 === 10; // Igaz, az implicit konverziónak hála

Ha egy natív típust mint a Number konstruktorként kezelünk, akkor egy új Number objektumot kapunk. De ha kihagyjuk a new kulcsszót akkor a Number egy egyszerű konverter függvényként fog viselkedni.

Ráadásul a literálok passzolgatásakor még több típuskonverzió üti fel a fejét.

A legjobb megoldás hogyha a három típus valamelyikére expliciten kasztolunk.

Stringre kasztolás

'' + 10 === '10'; // igaz

Egy üres string hozzáfűzésével könnyen tudunk egy értéket stringgé kasztolni.

Számra kaszt

+'10' === 10; // igaz

Az unáris plusz operátor használatával lehetséges egy értéket számra alakítani.

Booleanre kasztolás

A nem operátor kétszeri alkalmazásával tudunk booleanné kasztolni.

!!'foo';   // igaz
!!'';      // hamis
!!'0';     // igaz
!!'1';     // igaz
!!'-1'     // igaz
!!{};      // igaz
!!true;    // igaz

Lényeg

Miért Ne Használjuk az eval-t

Az eval (evil) funkció egy stringbe ágyazott JavaScript kódot futtat a lokális scopeon belül.

var foo = 1;
function test() {
    var foo = 2;
    eval('foo = 3');
    return foo;
}
test(); // 3
foo; // 1

Viszont az eval csak akkor viselkedik így, hogyha expliciten hívjuk meg és a meghívott funkció neve valóban eval.

var foo = 1;
function test() {
    var foo = 2;
    var bar = eval;
    bar('foo = 3');
    return foo;
}
test(); // 2
foo; // 3

Az eval használata kerülendő. A "felhasználása" az esetek 99.9%-ban mellőzhető.

Az eval ezer arca

A setTimeout és setInterval nevű timeout függvények is tudnak úgy működni, hogy első paraméterükként egy stringbe ágyazott kódot várnak. Ez a string mindig a globális hatókörben lesz végrehajtva, mivel az evalt így nem direktben hívjuk meg.

Biztonsági problémák

Az eval azért is veszélyes, mert bármilyen JS kódot végrehajt, amit odaadunk neki. Éppen ezért sose használjuk olyan kódok végrehajtására amiknek az eredete nem megbízható/ismeretlen.

Összegzésül

Soha ne használjunk evalt. Bármilyen kód működése, teljesítménye, ill. biztonsága megkérdőjelezhető amely használja ezt a nyelvi elemet. Semmilyen megoldás használata nem ajánlott amely első sorban evalra épül. Ekkor egy jobb megoldás szükségeltetik, amely nem függ az evaltól.

Az undefined és a null

A JavaScript két értéket is tartogat a semmi kifejezésére, ezek a null és az undefined és ezek közül az utóbbi a hasznosabb.

Az undefined

Ha az előbbi bevezetőtől nem zavarodtál volna össze; az undefined egy típus amelynek pontosan egy értéke van, az undefined.

A nyelvben szintén van egy undefined nevű globális változó amelynek az értékét hogy-hogy nem undefined-nak hívják. Viszont ez a változó nem konstans vagy kulcsszó a nyelvben. Ez azt jeletni hogy az értéke könnyedén felülírható.

Itt van pár példa, hogy mikor is találkozhatunk az undefined értékkel:

  • Az undefined globális változó elérésekor
  • Egy deklarált, de nem inicializált változó elérésekor.
  • Egy függvény hívásakor ez a visszatérési érték, return utasítás híján.
  • Egy olyan return utasítás lefutásakor, amely nem térít vissza értéket.
  • Nem létező mezők lekérésekor.
  • Olyan függvény paraméterek elérésekor amelyeknek a hívó oldalon nem kaptak értéket.
  • Bármikor amikor az undefined érték van valaminek beállítva.
  • Bármelyik void(kifejezés) utasítás futtatásakor.

undefined megőrzési trükkök

Mivel az undefined nevű globális változó csak egy másolatot tárol az undefined elnevezésű értékből, az értékének megváltoztatása nem írja felül az eredeti undefined típus értékét.

Ezért, ha valamilyen értékkel össze szeretnénk hasonlítani az undefined értéket, nem árt hogyha először magát az undefined-ot el tudjuk érni.

Egy gyakori technika annak érdekében hogy megvédjük a kódunkat az undefined lehetséges felüldefiniálásaitól, hogy egy névtelen (wrapper) függvénybe csomagoljuk az egész kódunkat, amelynek lesz egy direkt üres paramétere.

var undefined = 123;
(function(something, foo, undefined) {
    // az undefined ebben a hatókörben 
    // megint valóban az `undefined` értékre referáll.

})('Hello World', 42);

Egy másik módja ennek, hogy használunk egy "üres" deklarációt a wrapper függvényen belül.

var undefined = 123;
(function(something, foo) {
    var undefined;
    ...

})('Hello World', 42);

Az egyetlen különbség ebben a változatban, hogyha minifikáljuk ezt a kódot, és nem definiálunk további változókat ezen a részen belül, akkor ezzel a változattal extra 4 byte "veszteséget" szenvedünk el.

Mikor használjunk nullt

Miközben az undefined a natív JavaScript megvalósításokban inkább a (más nyelvekben levő) tradícionális null helyett használandó, azalatt maga a null inkább csak egy különböző adattípusnak számít, mindenféle különös jelentés nélkül.

Egy pár belső JavaScriptes megoldásban ugyan használják (ahol pl. a prototípus lánc végét a Foo.prototype = null beállítással jelölik), de a legtöbb esetben ez felcserélhető az undefined-al.

(A ford.: A null annak az esetnek a jelölésére hasznos, amikor egy referencia típusú változót deklarálunk, de még nem adunk neki értéket. Pl. a var ezObjektumLesz = null kifejezés ezt jelöli. Tehát a null leginkább kezdeti értékként állja meg a helyét, minden másra ott az undefined)

Automatic Semicolon Insertion

Bár a JavaScriptnek látszólag C-s szintaxisa van, mégsem kötelező benne kirakni a pontosvesszőket, így (helyenként) kihagyhatóak a forrásból. (A ford.: hiszen interpretált nyelv lévén nincsenek fordítási hibák, így nyelvi elemek meglétét sem tudja erőltetni a nyelv)

Itt jön a csel, hogy ennek ellenére a JavaScript csak pontosvesszőkkel értelmezi megfelelően a beírt kódot. Következésképp, a JS automatikusan illeszti be a pontosvesszőket (megpróbálja kitalálni a gondolataink) azokra a helyekre, ahol amúgy emiatt értelmezési hibába futna.

var foo = function() {
} // értelmezési hiba, pontosvessző kéne
test()

Az automatikus beillesztés megtörténik, ezután így értelmeződik a kód

var foo = function() {
}; // nincs hiba, mindenki örül
test()

Az automatikus beillesztés (ASI) a JavaScript (egyik) legnagyobb design hibája, mivel igen... meg tudja változtatni a kód értelmezését

Hogyan Működik

Az alábi kódban nincsen pontosvessző, így az értelmező (parser) feladata kitalálni, hogy hova is illessze be őket.

(function(window, undefined) {
    function test(options) {
        log('testing!')

        (options.list || []).forEach(function(i) {

        })

        options.value.test(
            'hosszú string az argumentumban',
            'még még még még még hossszabbbbbbb'
        )

        return
        {
            foo: function() {}
        }
    }
    window.test = test

})(window)

(function(window) {
    window.someLibrary = {}

})(window)

Alább mutatjuk a "kitalálós" játék eredményét.

(function(window, undefined) {
    function test(options) {

        // Nincs beillesztés, a sorok össze lettek vonva
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- beillesztés

        options.value.test(
            'hosszú string az argumentumban',
            'még még még még még hossszabbbbbbb'
        ); // <- beillesztés

        return; // <- beillesztés, eltörik a return kifejezésünk
        { // blokként értelemződik

            // név: kifejezés formátumban értelmeződik
            foo: function() {} 
        }; // <- beillesztés
    }
    window.test = test; // <- beillesztés

// Ezeket a sorokat összeilleszti
})(window)(function(window) {
    window.someLibrary = {}; // <- beillesztés

})(window); //<- beillesztés

Az értelmező drasztikusan megváltoztatta a fenti kódot. A legtöbb esetben a beillesztő rosszul tippel.

(A ford.: Semmilyen nyelvben sem jó, hogyha hagyjuk hogy a gép találja ki mit szerettünk volna írni. Néma gyereknek az anyja sem érti a kódját ugye)

Kezdő Zárójelek

Az értelmező nem rak be új pontosvesszőt, hogyha a sor eleje (nyitó) zárójellel kezdődik.

log('testing!')
(options.list || []).forEach(function(i) {})

Ez a kód egy sorként értelmeződik

log('testing!')(options.list || []).forEach(function(i) {})

Az esélyek arra elég magasak, hogy a log nem egy függvényt fog visszatéríteni; így a fenti kód egy TypeError típusú hibát fog dobni undefined is not a function üzenettel.

Összefoglalásképp

Szükségszerűen soha ne hagyjuk ki a pontoszvesszőket. Nem árt a kapcsos zárójeleket is ugyanazon a soron tartani, mint amelyiken az utasítást elkezdtük, így nem ajánlott az egysoros if / else kifejezések kedvéért elhagyni őket. Ezek a szempontok nem csak a kódot (és annak olvashatóságát) tartják konzisztensen, de megelőzik azt is hogy a JavaScript értelmező valamit rosszul "találjon ki".

A delete Operátor

Röviden, lehetetlen globális változókat, függvényeket és olyan dolgokat törölni JavaScriptben amelyeknek a DontDelete attribútuma be van állítva.

Globális kód és Függvény kód

Amikor egy változó/függvény, globális vagy függvény hatókörben van definiálva, akkor az vagy az Aktivációs (Activation) vagy a Globális (Global) objektum egyik mezőjeként jön létre. Az ilyen mezőknek van egy halom attribútuma, amelyek közül az egyik a DontDelete. A változó és függvény deklarációk a globális vagy függvény kódon belül mindig DontDelete tulajdonságú mezőket hoznak létre, így nem lehet őket törölni.

// globális változó
var a = 1; // A DontDelete be lett állítva
delete a; // hamis
a; // 1

// függvény:
function f() {} // A DontDelete be lett állítva
delete f; // hamis
typeof f; // "function"

// új értékadással sem megy
f = 1;
delete f; // hamis
f; // 1

Explicit mezők

Az expliciten beállított mezőket persze normálisan lehet törölni.

// expliciten beállított mező
var obj = {x: 1};
obj.y = 2;
delete obj.x; // igaz
delete obj.y; // igaz
obj.x; // undefined
obj.y; // undefined

A fenti példábna az obj.x és obj.y törölhető, mivel nincs DontDelete attribútuma egyik mezőnek sem. Ezért működik az alábbi példa is.

// működik, kivéve IE-ben
var GLOBAL_OBJECT = this;
GLOBAL_OBJECT.a = 1;
a === GLOBAL_OBJECT.a; // igaz - egy globális változó
delete GLOBAL_OBJECT.a; // igaz
GLOBAL_OBJECT.a; // undefined

Itt egy trükköt használunk az a törlésére. A this itt a Globális objektumra mutat, és expliciten bezetjük rajta az a változót, mint egy mezőjét, így törölni is tudjuk.

Mint az szokás, a fenti kód egy kicsit bugos IE-ben (legalábbis 6-8-ig).

Függvény argumentumok és beépített dolgaik

A függvény argumentumok, az arguments objektum és a beépített mezők szintén DontDelete tulajdonságúak.

// függvény argumentumok és mezők
(function (x) {

  delete arguments; // hamis
  typeof arguments; // "object"

  delete x; // hamis
  x; // 1

  function f(){}
  delete f.length; // hamis
  typeof f.length; // "number"

})(1);

Vendég (host) objektumok

A delete operátor működése megjósolhatatlan a vendég objektumokra. A specifikáció szerint ezek az objektumok szükség szerint bármilyen viselkedést implementálhatnak.

(A ford.: Vendég objektumok azok az objektumok, amelyek nincsenek konkrétan meghatározva az ES aktuális verziójú specifikációjában, pl. a window)

Összegzésképp

A delete működése helyenként megjósolhatatlan, így biztonsággal csak olyan objektumok mezőin használhatjuk amelyeket expliciten mi állítottunk be.

Egyéb

A varázslatos setTimeout és setInterval

Mivel a JavaScript aszinkron, a setTimeout és setInterval használatával lehetséges késleltetni a kódok lefutási idejét.

function foo() {}
var id = setTimeout(foo, 1000); // Egy számmal (> 0) tér vissza

Amikor a setTimeout függvényt meghívjuk, válaszul egy timeout ID-t kapunk valamint be lesz ütemezve a foo függvényhívás, hogy körülbelül 1000 miliszekundum múlva fusson le a jövőben. A foo egyszer lesz végrehajtva.

Az aktuális JavaScript motor időzítésétől függően, és annak figyelembe vételével hogy a JavaScript mindig egyszálú, tehát a megelőző kódok blokkolhatják a szálat, soha nem lehet biztonságosan meghatározni hogy valóban a kért időzítéssel fog lefutni a kód amit megadtunk a setTimeoutban. Erre semmilyen biztosíték nincs.

Az első helyen bepasszolt függvény a globális objektum által lesz meghívva, ami azt jelenti hogy a this a függvényen belül a globális objektumra utal.

function Foo() {
    this.value = 42;
    this.method = function() {
        // a this egy globális objektumra utal, nem a Foo-ra
        console.log(this.value); // undefined-ot logol ki
    };
    setTimeout(this.method, 500);
}
new Foo();

Híváshalmozás a setIntervalal

Míg a setTimeout csak egyszer futtatja le a megadott függvényt, a setInterval

  • ahogy a neve is mutatja - minden X miliszekundumban végrehajtja a neki átadott kódot, használata pedig erősen kerülendő.

Nagy hátulütője, hogy még akkor is ütemezi az újabb és újabb hívásokat, hogyha az aktuálisan futattot kód a megadott időintervallumon felül blokkolja a további kód futtatást. Ez, hogyha megfelelően rövid intervallumokat állítunk be, felhalmozza a függvényhívásokat a call stacken.

function foo(){
    // kód ami 1 másodpercig feltartja a futtatást
}
setInterval(foo, 100);

A fenti kódban amikor a foo meghívódik, 1 másodpercig feltartja a további futtatást.

A setInterval persze ütemezni fogja a jövőbeli foo hívásokat továbbra is, amíg blokkolódik a futtatás. Így tíz további hívás fog várakozni, miután a foo futtatása először végzett.

Hogyan Bánjunk El a Blokkolással

A legkönnyebb és kontrollálhatóbb megoldásnak az bizonyul, hogyha a setTimeout függvényt a rögtön a foo-n belül használjuk.

function foo(){
    // 1 másodpercig blokkoló kód
    setTimeout(foo, 100);
}
foo();

Ez nem csak egységbe zárja a setTimeout hívást, de meggátolja a felesleges hívások felhalmozását, és több irányítást ad a kezünkbe. A foo így magától eltudja dönteni, hogy akarja-e újra futtatni önmagát vagy sem.

Timeout Tisztogatás Kézzel

A clearTimeout vagy clearInterval hívással tudjuk a timeoutjainkat megszüntetni, természetesen attól függ hogy melyiket használjuk, hogy melyik set függvénnyel indítottuk útjára a timeoutunkat.

var id = setTimeout(foo, 1000);
clearTimeout(id);

Az Összes Timeout Megszüntetése

Mivel nincsen beépített megoldás az összes timeout és/vagy interval hívás törlésére, ezért bruteforce módszerekhez kell folyamodjunk.

// az "összes" timeout kitörlése
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}

Persze ez csak véletlenszerű lövöldözés, semmi sem garantálja hogy a fenti módszerrel nem marad timeout a rendszerben (A ford.: például az ezredik timeout vagy afelett). Szóval egy másik módszer ennek megoldására, hogy feltételezzük hogy minden setTimeout hívással az azonosítók száma egyel növekszik.

// az "összes" timeout kiírtása
var legnagyobbTimeoutId = window.setTimeout(function(){}, 1),
i;
for(i = 1; i <= legnagyobbTimeoutId; i++) {
    clearTimeout(i);
}

Habár ez a megoldás minden böngészőben megy (egyenlőre), ez az azonosítókról született mondás nincs specifikációban rögzítve, és ennek megfelelően változhat. Az ajánlott módszer továbbra is az, hogy kövessük nyomon az összes timeout azonosítót amit generáltunk, és így ki is tudjuk őket rendesen törölni.

eval A Színfalak Mögött

Habár a setTimeout és a setInterval (kód) stringet is tud első paramétereként fogdani, ezt a fajta formáját használni kimondottan tilos, mivel a függöny mögött ő is csak evalt használ.

function foo() {
    // meg lesz hívva
}

function bar() {
    function foo() {
        // soha nem hívódik meg
    }
    setTimeout('foo()', 1000);
}
bar();

Mivel az evalt nem direkt módon hívjuk meg a fenti esetben, a setTimeoutnak passzolt string a globális hatókörben fog lefutni; így a lokális foo függvényt sosem használjuk a bar hatóköréből.

Továbbá nem ajánlott argumentumokat átadni annak a függvénynek amelyik a timeout függvények által meg lesz hívva a későbbiekben.

function foo(a, b, c) {}

// SOHA ne használd így!
setTimeout('foo(1, 2, 3)', 1000)

// Ehelyett csomagoljuk névtelen függvénybe
setTimeout(function() {
    foo(1, 2, 3);
}, 1000)

Összegzésképp

Soha ne használjunk stringeket a setTimeout vagy setInterval első paramétereiként. Ha argumentumokat kell átadni a meghívandó függvénynek, az egyértelműen rossz kódra utal. Ebben az esetben a függvényhívás lebonyolításához egy anoním függvény használata ajánlott.

Továbbá, mivel az ütemező kódja nem blokkolódik a JavaScript futás által, a setInterval használata úgy általában kerülendő.