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.
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.
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 mindenfor 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.
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étrehozottObjektumra 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 Counterké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áliscount 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.
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.
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.
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.
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ő nemugyanazt jelenti: a var elhagyásának jópár
beláthatatlan következménye is lehet.
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 googlobá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.
Abban az esetben ha találunk var foo utasítást, használjuk azt.
Hogyha bármelyik függvény paraméter neve foo, használjuk azt.
Hogyha magának a függvénynek a neve `foo, használjuk azt.
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.
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.
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.
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ésmelegen 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 undefinedtí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.
Az esélyek arra elég magasak, hogy a lognem 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.
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 fooegyszer 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 - mindenX 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ő.