Johdanto

Tekijät

Tämä opas pohjautuu kahden mukavan Stack Overflow käyttäjän työhön. He ovat Ivo Wetzel (kirjoittaminen) sekä Zhang Yi Jiang (ulkoasu).

Osallistujat

Kääntäjät

Lisenssi

JavaScript-puutarha on julkaistu MIT-lisenssin-alaisena ja se on saatavilla GitHubissa. Mikäli löydät virheitä, lisää se seurantajärjestelmään tai tee pull-pyyntö. Löydät meidät myös JavaScript huoneesta Stack Overflown chatista.

Oliot

Olioiden käyttö ja ominaisuudet

Kaikki muuttujat, kahta poikkeusta lukuunottamatta, käyttäytyvät JavaScriptissä oliomaisesti. Nämä poikkeukset ovat null sekä undefined.

false.toString(); // epätosi
[1, 2, 3].toString(); // '1,2,3'

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

Yleisesti luullaan ettei numeroliteraaleja voida käyttää olioina. Tämä johtuu viasta JavaScriptin parserissa. Se yrittää parsia numeron pistenotaatiota liukulukuliteraalina.

2.toString(); // palauttaa SyntaxError-virheen

Tämä voidaan välttää esimerkiksi seuraavasti.

2..toString(); // toinen piste tunnistuu oikein
2 .toString(); // huomaa pisteen vasemmalla puolen oleva väli
(2).toString(); // 2 arvioidaan ensi

Oliot tietotyyppinä

JavaScriptin olioita voidaan käyttää myös hajautustauluna, koska ne muodostavat pääasiassa avaimien ja niihin liittyvien arvojen välisen mappauksen.

Olioliteraalinotaatiota - {} - käyttäen voidaan luoda tyhjä olio. Tämä olio perii Object.prototype-olion eikä sille ole määritelty omia ominaisuuksia.

var foo = {}; // uusi, tyhjä olio

// uusi, tyhjä olio, joka sisältää ominaisuuden 'test' arvolla 12
var bar = {test: 12}; 

Pääsy ominaisuuksiin

Olion ominaisuuksiin voidaan päästä käsiksi kahta eri tapaa käyttäen. Siihen voidaan käyttää joko piste- tai hakasulkunotaatiota.

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

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

foo.1234; // SyntaxError
foo['1234']; // toimii

Kumpikin notaatio toimii samalla tavoin. Ainut ero liittyy siihen, että hakasulkunotaation avulla ominaisuuksien arvoja voidaan asettaa dynaamisesti. Se sallii myös muuten hankalien, virheeseen johtavien nimien käyttämisen.

Ominaisuuksien poistaminen

Ainut tapa poistaa olion ominaisuus on käyttää delete-operaattoria. Ominaisuuden asettaminen joko arvoon undefined tai null poistaa vain siihen liittyneen arvon muttei itse avainta.

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]);
    }
}

Yllä oleva koodi tulostaa sekä both undefined että foo null. Ainoastaan baz on poistettu. Täten sitä ei myöskään näy tulosteessa.

Avainnotaatio

var test = {
    'case': 'Olen avainsana, joten minun tulee olla merkkijono',
    delete: 'Myös minä olen avainsana' // palauttaa SyntaxError-virheen
};

Olioiden ominaisuuksia voidaan notatoida käyttäen joko pelkkiä merkkejä tai merkkijonoja. Toisesta JavaScriptin suunnitteluvirheestä johtuen yllä oleva koodi palauttaa SyntaxError-virheen ECMAScript 5:ttä edeltävissä versioissa.

Tämä virhe johtuu siitä, että delete on avainsana. Täten se tulee notatoida merkkijonona. Tällöin myös vanhemmat JavaScript-tulkit ymmärtävät sen oikein.

Prototyyppi

JavaScript ei sisällä klassista perintämallia. Sen sijaan se käyttää prototyyppeihin pohjautuvaa ratkaisua.

Usein tätä pidetään JavaScriptin eräänä suurimmista heikkouksista. Itse asiassa prototyyppipohjainen perintämalli on voimakkaampi kuin klassinen malli. Sen avulla voidaan mallintaa klassinen malli melko helposti. Toisin päin mallintaminen on huomattavasti vaikeampaa.

JavaScript on käytännössä ainut laajasti käytetty kieli, joka tarjoaa tuen prototyyppipohjaiselle perinnälle. Tästä johtuen mallien väliseen eroon tottuminen voi viedä jonkin akaa.

Ensimmäinen suuri ero liittyy siihen, kuinka perintä toimii. JavaScriptissä se pohjautuu erityisiin prototyyppiketjuihin.

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

function Bar() {}

// Aseta Barin prototyypin uuteen Foo-olioon
Bar.prototype = new Foo();
Bar.prototype.foo = 'Terve maailma';

// Huolehdi siitä, että Bar on todellinen konstruktori
Bar.prototype.constructor = Bar;

var test = new Bar() // luo uusi bar

// Prototyyppiketju
test [Bar-olio]
    Bar.prototype [Foo-olio] 
        { foo: 'Terve maailma', value: 42 }
        Foo.prototype
            { method: ... }
            Object.prototype
                { toString: ... /* jne. */ }

Yllä olio test perii sekä Bar.prototype- että Foo.prototype-olion. Tällöin se pääsee käsiksi Foo:ssa määriteltyy funktioon method. Se pääsee käsiksi myös ominaisuuteen value, jonka luotu Foo-olio sisältää prototyypissään. On tärkeää huomata, että new Bar() ei luo uutta Foo-oliota vaan käyttää uudelleen sen prototyyppiin asetettua. Tässä tapauksessa kaikki Bar-oliot jakavat siis saman value-ominaisuuden.

Ominaisuushaut

Kun olion ominaisuuksien arvoa haetaan, JavaScript käy prototyyppiketjua läpi ylöspäin, kunnes se löytää ominaisuuden nimeä vastaavan arvon.

Jos se saavuttaa ketjun huipun - Object.prototype-olion - eikä ole vieläkään löytänyt haettua ominaisuutta, se palauttaa undefined arvon sen sijaan.

Prototyyppi-ominaisuus

Vaikka Prototyyppi-ominaisuutta käytetään prototyyppiketjujen rakentamiseen, voidaan siihen asettaa mikä tahansa arvo. Mikäli arvo on primitiivi, se yksinkertaisesti jätetään huomiotta.

function Foo() {}
Foo.prototype = 1; // ei vaikutusta

Kuten esimerkissä yllä, prototyyppiin on mahdollista asettaa olioita. Tällä tavoin prototyyppiketjuja voidaan koostaa dynaamisesti.

Suorituskyky

Prototyyppiketjussa korkealla olevien ominaisuuksien hakeminen voi hidastaa koodin kriittisiä osia. Tämän lisäksi olemattomien ominaisuuksien hakeminen käy koko ketjun läpi.

Ominaisuuksia iteroidessa prototyyppiketjun jokainen ominaisuus käydään läpi.

Natiivien prototyyppien laajentaminen

JavaScript mahdollistaa Object.prototype-olion sekä muiden natiivityyppien laajentamisen.

Tätä tekniikkaa kutsutaan nimellä apinapätsäämiseksi. Se rikkoo kapseloinnin. Vaikka yleisesti käytetyt alustat, kuten Prototype, käyttävätkin sitä, ei ole olemassa yhtään hyvää syytä, minkä takia natiivityyppejä tulisi laajentaa epästandardilla* toiminnallisuudella.

Ainut hyvä syy on uudempien JavaScript-tulkkien sisältämien ominaisuuksien siirtäminen vanhemmille alustoille. Eräs esimerkki tästä on Array.forEach.

Yhteenveto

Ennen kuin kirjoitat monimutkaista prototyyppiperintää hyödyntävää koodia, on olennaista, että ymmärrät täysin kuinka se toimii. Ota huomioon myös prototyyppiketjujen pituus ja riko niitä tarpeen mukaan välttääksesi suorituskykyongelmia. Huomioi myös, että natiiveja prototyyppejä ei tule laajentaa milloinkaan ellei kyse ole vain yhteensopivuudesta uudempien JavaScript-ominaisuuksien kanssa.

hasOwnProperty

Jotta voimme tarkistaa onko olion ominaisuus määritelty siinä itsessään, tulee käyttää erityistä Object.prototype-oliosta periytyvää hasOwnProperty-metodia. Tällä tavoin vältämme prototyyppiketjun sisältämät ominaisuudet.

hasOwnProperty on ainut JavaScriptin sisältämä metodi, joka käsittelee ominaisuuksia eikä käy prototyyppiketjun sisältöä läpi.

// Object.prototypen myrkyttäminen
Object.prototype.bar = 1; 
var foo = {goo: undefined};

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

foo.hasOwnProperty('bar'); // epätosi
foo.hasOwnProperty('goo'); // tosi

Ainoastaan hasOwnProperty palauttaa oikean ja odotetun tuloksen. Sen tietäminen on olennaista minkä tahansa olion ominaisuuksia iteroidessa. Tämä on ainut tapa löytää olion itsensä ominaisuudet prototyyppiketjusta riippumatta.

hasOwnProperty ominaisuutena

JavaScript ei suojele hasOwnProperty-metodin nimeä. Täten on mahdollista, että olio voi sisältää samannimisen ominaisuuden. Jotta voimme saada oikeita tuloksia, tulee sen sijaan käyttää ulkoista hasOwnProperty-metodia.

var foo = {
    hasOwnProperty: function() {
        return false;
    },
    bar: 'Olkoon vaikka lohikäärmeitä'
};

foo.hasOwnProperty('bar'); // palauttaa aina epätoden

// Käytä toisen olion hasOwnProperty-metodia ja kutsu sitä asettamalla
// 'this' foohon
({}).hasOwnProperty.call(foo, 'bar'); // tosi

Yhteenveto

Mikäli pitää selvittää kuuluuko ominaisuus olioon vai ei, ainoastaan hasOwnProperty voi kertoa sen. Tämän lisäksi on suositeltavaa käyttää hasOwnProperty-metodia osana jokaista for in-luuppia. Tällä tavoin voidaan välttää natiivien prototyyppien laajentamiseen liittyviä ongelmia.

for in-luuppi

Aivan kuten in-operaattori, myös for in-luuppi käy olion prototyyppiketjun läpi iteroidessaan sen ominaisuuksia.

// Object.prototypen myrkyttäminen
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
    console.log(i); // tulostaa sekä bar että moo
}

Koska for in-luupin käytöstapaa ei voida muokata suoraan, tulee ei-halutut ominaisuudet karsia itse luupin sisällä. Tämä on mahdollista käyttäen Object.prototype-olion hasOwnProperty-metodia.

hasOwnProperty-metodin käyttäminen karsimiseen

// foo kuten yllä
for(var i in foo) {
    if (foo.hasOwnProperty(i)) {
        console.log(i);
    }
}

Tämä versio on ainut oikea. Se tulostaa ainoastaan moo, koska se käyttää hasOwnProperty-metodia oikein. Kun se jätetään pois, on koodi altis virheille tapauksissa, joissa prototyyppejä, kuten Object.prototype, on laajennettu.

Prototype on eräs yleisesti käytetty ohjelmointialusta, joka tekee näin. Kun kyseistä alustaa käytetään, for in-luupit, jotka eivät käytä hasOwnProperty-metodia, menevät varmasti rikki.

Yhteenveto

On suositeltavaa käyttää aina hasOwnProperty-metodia. Ei ole kannattavaa tehdä ajoympäristöön tai prototyyppeihin liittyviä oletuksia.

Funktiot

Funktiomääreet ja lausekkeet

JavaScriptissä funktiot ovat ensimmäisen luokan olioita. Tämä tarkoittaa sitä, että niitä voidaan välittää kuten muitakin arvoja. Usein tätä käytetään takaisinkutsuissa käyttämällä nimettömiä, mahdollisesti asynkronisia funktioita.

function-määre

function foo() {}

Yllä oleva funktio hilataan ennen ohjelman suorituksen alkua. Se näkyy kaikkialle näkyvyysalueessaan, jossa se on määritelty. Tämä on totta jopa silloin, jos sitä kutsutaan ennen määrittelyään.

foo(); // Toimii, koska foo on luotu ennen kuin koodi suoritetaan
function foo() {}

function-lauseke

var foo = function() {};

Tämä esimerkki asettaa nimeämättömän ja nimettömän funktion muuttujan foo arvoksi.

foo; // 'undefined'
foo(); // tämä palauttaa TypeError-virheen
var foo = function() {};

var on määre. Tästä johtuen se hilaa muuttujanimen foo ennen kuin itse koodia ryhdytään suorittamaan.

Sijoituslauseet suoritetaan vasta kun niihin saavutaan. Tästä johtuen foo saa arvokseen undefined ennen kuin varsinaista sijoitusta päästään suorittamaan.

Nimetty funktiolauseke

Nimettyjen funktioiden sijoitus tarjoaa toisen erikoistapauksen.

var foo = function bar() {
    bar(); // Toimii
}
bar(); // ReferenceError

Tässä tapauksessa bar ei ole saatavilla ulommalla näkyvyysalueessa. Tämä johtuu siitä, että se on sidottu foo:n sisälle. Tämä johtuu siitä, kuinka näkyvyysalueet ja niihin kuuluvat jäsenet tulkitaan. Funktion nimi on aina saatavilla sen paikallisessa näkyvyysalueessa itsessään.

Kuinka this toimii

JavaScripting this toimii eri tavoin kuin useimmissa kielissä. Tarkalleen ottaen on olemassa viisi eri tapaa, joiden mukaan sen arvo voi määrittyä.

Globaali näkyvyysalue

this;

Kun this-muuttujaa käytetään globaalissa näkyvyysalueessa, viittaa se globaaliin olioon.

Funktiokutsu

foo();

Tässä tapauksessa this viittaa jälleen globaaliin olioon.

Metodikutsu

test.foo(); 

Tässä esimerkissä this viittaa test-olioon.

Konstruktorikutsu

new foo(); 

Funktiokutsu, jota edeltää new-avainsana toimii konstruktorina. Funktion sisällä this viittaa juuri luotuun Object-olioon.

this-arvon asettaminen

function foo(a, b, c) {}

var bar = {};
foo.apply(bar, [1, 2, 3]); // taulukko laajenee alla olevaksi
foo.call(bar, 1, 2, 3); // tuloksena a = 1, b = 2, c = 3

Function.prototype-olion call- ja apply-metodeita käytettäessä this-ominaisuuden arvo määrittyy ensimmäisen annetun argumentin perusteella.

Seurauksena foo-funktion sisältämä this asettuu bar-olioon toisin kuin perustapauksessa.

Yleisiä ongelmakohtia

Useimmat näistä tapauksista ovat järkeviä. Ensimmäistä niistä tosin voidaan pitää suunnitteluvirheenä, jolle ei ole mitään järkevää käyttöä ikinä.

Foo.method = function() {
    function test() {
        // this asettuu globaaliin olioon
    }
    test();
};

Yleisesti luullaan, että test-funktion sisältämä this viittaa tässä tapauksessa Foo-olioon. Todellisuudessa se ei kuitenkaan tee näin.

Jotta Foo-olioon voidaan päästä käsiksi test-funktion sisällä, tulee metodin sisälle luoda paikallinen muuttuja, joka viittaa Foo-olioon.

Foo.method = function() {
    var that = this;
    function test() {
        // Käytä thatia thissin sijasta
    }
    test();
};

that on normaali nimi, jota käytetään yleisesti viittaamaan ulompaan this-muuttujaan. Sulkeumia käytettäessä this-arvoa voidaan myös välittää edelleen.

Metodien sijoittaminen

JavaScriptissä funktioita ei voida nimetä uudelleen eli siis sijoittaa edelleen.

var test = someObject.methodTest;
test();

Ensimmäisestä tapauksesta johtuen test toimii kuten normaali funktiokutsu; tällöin sen sisältämä this ei enää osoita someObject-olioon.

Vaikka this-arvon myöhäinen sidonta saattaa vaikuttaa huonolta idealta, se mahdollistaa prototyyppeihin pohjautuvan perinnän.

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

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

new Bar().method();

Kun method-metodia kutsutaan Bar-oliossa, sen this viittaa juurikin tuohon olioon.

Sulkeumat ja viitteet

Sulkeumat ovat eräs JavaScriptin voimakkaimmista ominaisuuksista. Näkyvyysalueilla on siis aina pääsy ulompaan näkyvyysalueeseensa. Koska JavaScriptissä ainut tapa määritellä näkyvyyttä pohjautuu funktionäkyvyyteen, kaikki funktiot käyttäytyvät oletuksena sulkeumina.

Paikallisten muuttujien emulointi

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

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

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

Tässä tapauksessa Counter palauttaa kaksi sulkeumaa. Funktion increment lisäksi palautetaan myös funktio get. Kumpikin funktio viittaa Counter-näkyvyysalueeseen ja pääsee siten käsiksi count-muuttujan arvoon.

Miksi paikalliset muuttujat toimivat

JavaScriptissä ei voida viitata näkyvyysalueisiin. Tästä seuraa ettei count-muuttujan arvoon voida päästä käsiksi funktion ulkopuolelta. Ainoastaan nämä kaksi sulkeumaa mahdollistavat sen.

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

Yllä oleva koodi ei muuta muuttujan count arvoa Counter-näkyvyysalueessa. Tämä johtuu siitä, että foo.hack-ominaisuutta ei ole määritelty kyseisessä näkyvyysalueessa. Sen sijaan se luo - tai ylikirjoittaa - globaalin muuttujan count.

Sulkeumat luupeissa

Usein sulkeumia käytetään väärin luuppien sisällä indeksimuuttujien arvon kopiointiin.

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

Yllä oleva koodi ei tulosta numeroita nollasta yhdeksään. Sen sijaan se tulostaa numeron 10 kymmenen kertaa.

Nimetön funktio saa viitteen i-muuttujaan console.log-kutsuhetkellä. Tällöin luuppi on jo suoritettu ja i:n arvoksi on asetettu 10.

Päästäksemme haluttuun lopputulokseen on tarpeen luoda kopio i:n arvosta.

Viiteongelman välttäminen

Voimme välttää ongelman käyttämällä nimetöntä käärettä.

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

Nimetöntä ulkofunktiota kutsutaan heti käyttäen i:tä se ensimmäisenä argumenttina. Tällöin se saa kopion i:n arvosta parametrina e.

Nimetön funktio, jolle annetaan setTimeout sisältää nyt viitteen e:hen, jonka arvoa luuppi ei muuta.

Samaan lopputulokseen voidaan päästä myös palauttamalla funktio nimettömästä kääreestä. Tällöin se käyttäytyy samoin kuten yllä.

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

arguments-olio

Jokainen JavaScriptin näkyvyysalue pääsee käsiksi erikoismuuttujaan nimeltään arguments. Tämä muuttuja sisältää listan kaikista funktiolle annetuista argumenteista.

arguments-olio ei ole Array. Sen semantiikka, erityisesti length-ominaisuus, muistuttaa taulukkoa. Tästä huolimatta se ei peri Array.prototype:stä ja on itse asiassa Object.

Tästä johtuen arguments-olioon ei voida soveltaa normaaleja taulukkometodeja, kuten push, pop tai slice. Vaikka iterointi onnistuukin for-luuppeja käyttäen, tulee se muuttaa aidoksi Array-olioksi ennen kuin siihen voidaan soveltaa näitä metodeja.

Array-olioksi muuttaminen

Alla oleva koodi palauttaa uuden Array-olion, joka sisältää arguments-olion kaikki jäsenet.

Array.prototype.slice.call(arguments);

Tämä muutos on luonteeltaan hidas eikä sitä suositella käytettävän suorituskykyä vaativissa osissa koodia.

Argumenttien antaminen

Funktiosta toiselle voidaan antaa argumentteja seuraavasti.

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // tee jotain
}

Toinen keino on käyttää sekä call- että apply-funktioita yhdessä ja luoda nopeita, sitomattomia kääreitä.

function Foo() {}

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

// Luo "metodin" sitomaton versio 
// Se ottaa seuraavat parametrit: this, arg1, arg2...argN
Foo.method = function() {

    // Tulos: Foo.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Foo.prototype.method, arguments);
};

Muodolliset parametrit ja argumenttien indeksit

arguments-olio luo sekä getter- että setter-funktiot sekä sen ominaisuuksille että myös funktion muodollisille parametreille.

Tästä seuraa, että muodollisen parametrin arvon muuttaminen muuttaa myös arguments-olion vastaavan ominaisuuden arvoa ja toisin päin.

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);

Suorituskykyyn liittyviä myyttejä ja totuuksia

arguments-olio luodaan aina paitsi jos se on jo julistettu nimenä funktiossa tai sen muodollisena parametrina. Tämä siitä huolimatta käytetäänkö sitä vai ei.

Sekä getter- ja setter-funktiot luodaan aina. Tästä seuraa, että niiden käytöllä ei ole juurikaan merkitystä suorituskyvyn kannalta.

On kuitenkin eräs tapaus, jossa suorituskyky kärsii. Tämä liittyy arguments.callee-ominaisuuden käyttöön.

function foo() {
    arguments.callee; // tee jotain tällä funktio-oliolla
    arguments.callee.caller; // ja kutsuvalla funktio-oliolla
}

function bigLoop() {
    for(var i = 0; i < 100000; i++) {
        foo(); // normaalisti tämä olisi inline-optimoitu
    }
}

Yllä olevassa koodissa foo-kutsua ei voida käsitellä avoimesti, koska sen tulee tietää sekä itsestään että kutsujasta. Sen lisäksi, että se haittaa suorituskykyä, rikkoo se myös kapseloinnin. Tässä tapauksessa funktio voi olla riippuvainen tietystä kutsuympäristöstä.

On erittäin suositeltavaa ettei arguments.callee-ominaisuutta tai sen ominaisuuksia käytetä ikinä.

Konstruktorit

JavaScriptin konstruktorit eroavat monista muista kielistä selvästi. Jokainen funktiokutsu, joka sisältää avainsanan new toimii konstruktorina.

Konstruktorin - kutsutun funktion - this-muuttujan arvo viittaa luotuun Object-olioon. Tämän uuden olion prototyyppi asetetaan osoittamaan konstruktorin kutsuman funktio-olion prototyyppiin.

Mikäli kutsuttu funktio ei sisällä selvää return-lausetta, tällöin se palauttaa this-muuttujan arvon eli uuden olion.

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

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

var test = new Foo();

Yllä Foo:ta kutsutaan konstruktorina. Juuri luodun olion prototyyppi asetetaan osoittamaan ominaisuuteen Foo.prototype.

Selvän return-lausekkeen tapauksessa funktio palauttaa ainoastaan määritellyn lausekkeen arvon. Tämä pätee tosin vain jos palautettava arvo on tyypiltään Object.

function Bar() {
    return 2;
}
new Bar(); // uusi olio

function Test() {
    this.value = 2;

    return {
        foo: 1
    };
}
new Test(); // palautettu olio

Mikäli new-avainsanaa ei käytetä, funktio ei palauta uutta oliota.

function Foo() {
    this.bla = 1; // asetetaan globaalisti
}
Foo(); // undefined

Vaikka yllä oleva esimerkki saattaa näyttää toimivan joissain tapauksissa, viittaa this globaalin olion this-ominaisuuteen.

Tehtaat

Mikäli new-avainsanan käyttöä halutaan välttää, voidaan konstruktori pakottaa palauttamaan arvo.

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

new Bar();
Bar();

Tässä tapauksessa molemmat Bar-funktion kutsut käyttäytyvät samoin. Kumpikin kutsu palauttaa olion, joka sisältää method-ominaisuuden. Kyseinen ominaisuus on sulkeuma.

On myös tärkeää huomata, että kutsu new Bar() ei vaikuta palautetun olion prototyyppiin. Vaikka luodun olion prototyyppi onkin asetettu, Bar ei palauta ikinä kyseistä prototyyppioliota.

Yllä olevassa esimerkissä new-avainsanan käytöllä tai käyttämällä jättämisellä ei ole toiminnan kannalta mitään merkitystä.

Tehtaiden käyttö uusien olioiden luomiseen

Usein suositellaan new-avainsanan käytön välttämistä. Tämä johtuu siitä, että sen käyttämättä jättäminen voi johtaa bugeihin.

Sen sijaan suositellaan käytettävän tehdasta, jonka sisällä varsinainen olio konstruoidaan.

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

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

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

Vaikka yllä oleva esimerkki välttää new-avainsanan käyttöä ja tekee paikallisten muuttujien käytön helpommaksi, sisältää se joitain huonoja puolia.

  1. Se käyttää enemmän muistia. Tämä johtuu siitä, että luodut oliot eivät jaa prototyypin metodeja.
  2. Perinnän tapauksessa tehtaan tulee kopioida toisen olion kaikki metodit tai vaihtoehtoisesti asettaa kyseinen olio toisen prototyypiksi.
  3. Prototyyppiketjun käsitteen unohtaminen on vain välttääksemme new-avainsanan käyttöä on vastoin kielen filosofista perustaa.

Yhteenveto

Vaikka new-avainsanan käyttö voi johtaa bugeihin, prototyyppien käyttöä ei kannata unohtaa kokonaan. Loppujen lopuksi kyse on siitä, kumpi tapa sopii sovelluksen tarpeisiin paremmin. On erityisen tärkeää valita jokin tietty tapa ja pitäytyä sen käytössä.

Näkyvyysalueet ja nimiavaruudet

Vaikka JavaScript-käyttääkin aaltosulkeita blokkien ilmaisuun, se ei tue blokkinäkyvyyttä. Tämä tarkoittaa sitä, että kieli tukee ainoastaan *funktionäkyvyyttä.

function test() { // näkyvyysalue
    for(var i = 0; i < 10; i++) { // tämä ei ole näkyvyysalue
        // count
    }
    console.log(i); // 10
}

JavaScript ei myöskään sisällä erityistä tukea nimiavaruuksille. Tämä tarkoittaa sitä, että kaikki määritellään oletuksena globaalissa nimiavaruudessa.

Joka kerta kun muuttujaan viitataan, JavaScript käy kaikki näkyvyysalueet läpi alhaalta lähtien. Mikäli se saavuttaa globaalin näkyvyystalueen, eikä löydä haettua nimeä, se palauttaa ReferenceError-virheen.

Riesa nimeltä globaalit muuttujat

// skripti A
foo = '42';

// skripti B
var foo = '42'

Yllä olevat skriptit käyttäytyvät eri tavoin. Skripti A määrittelee muuttujan nimeltä foo globaalissa näkyvyysalueessa. Skripti B määrittelee foo-muuttujan vallitsevassa näkyvyysalueessa.

Tämä ei ole sama asia. var-avainsanan käyttämättä jättäminen voi johtaa vakaviin seurauksiin.

// globaali näkyvyysalue
var foo = 42;
function test() {
    // paikallinen näkyvyysalue
    foo = 21;
}
test();
foo; // 21

var-avainsanan pois jättäminen johtaa siihen, että funktio test ylikirjoittaa foo:n arvon. Vaikka tämä ei välttämättä vaikutakaan suurelta asialta, tuhansien rivien tapauksessa var-avainsanan käyttämättömyys voi johtaa vaikeasti löydettäviin bugeihin.

// globaali näkyvyysalue
var items = [/* joku lista */];
for(var i = 0; i < 10; i++) {
    subLoop();
}

function subLoop() {
    // aliluupin näkyvyysalue
    for(i = 0; i < 10; i++) { // hups, var jäi pois
        // jotain makeaa ja hienoa
    }
}

Tässä tapauksessa ulomman luupin suoritus lopetetaan ensimmäisen subLoop-kutsun jälkeen. Tämä johtuu siitä, että se ylikirjoittaa i:n globaalin arvon. Mikäli jälkimmäisessä luupissa olisi käytetty var-avainsanaa, olisi ikävyyksiltä vältytty. var-avainsanaa ei siis tule ikinä jättää pois ellei siihen ole hyvää syytä.

Paikalliset muuttujat

Ainoastaan funktion parametrit ja muuttujat, jotka sisältävät var-määreen ovat paikallisia.

// globaali näkyvyysalue
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
    // paikallinen näkyvyysalue
    i = 5;

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

foo ja i ovatkin test-funktiolle paikallisia. bar sijoitus muuttaa globaalin muuttujan arvoa.

Hilaaminen

JavaScript hilaa määreitä. Tämä tarkoittaa sitä, että sekä var-lausekkeet että function-määreet siirretään ne sisältävän näkyvyysalueen huipulle.

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];
    }
}

Yllä olevaa koodia muutetaan ennen suoritusta. JavaScript siirtää var-lausekkeet ja function-määreet lähimmän näkyvyysalueen huipulle.

// var-lausekkeet siirrettiin tänne
var bar, someValue; // oletuksena 'undefined'

// myös funktio-määre siirtyi tänne
function test(data) {
    var goo, i, e; // ei blokkinäkyvyyttä, siirretään siis tänne
    if (false) {
        goo = 1;

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

bar(); // TypeError-virhe, baria ei ole vielä määritelty
someValue = 42; // hilaus ei koske sijoituksia
bar = function() {};

test();

Sen lisäksi, että puuttuva blokkinäkyvyys siirtää var-lausekkeet luuppien ulkopuolelle, tekee se myös eräistä if-rakenteista vaikeita käsittää.

Alkuperäisessä koodissa if-lause näytti muokkaavan globaalia muuttujaa goo. Todellisuudessa se muokkaa paikallista muuttujaa varsinaisen hilauksen jälkeen.

Seuraava koodi saattaisi ensi näkemältä aiheuttaa ReferenceError-virheen. Näin ei kuitenkaan tapahdu hilauksen ansiosta.

// onko SomeImportantThing alustettu
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}

Tämä toimii, koska var-lauseke on hilattu globaalin näkyvyysalueen huipulle.

var SomeImportantThing;

// mahdollista alustuskoodia

// onhan se alustettu
if (!SomeImportantThing) {
    SomeImportantThing = {};
}

Nimienerottelujärjestys

Kaikki JavaScriptin näkyvyysalueet, globaalin näkyvyysalue mukaanlukien, sisältävät erikoismuuttujan this. this viittaa tämänhetkiseen olioon.

Funktioiden näkyvyysalueet sisältävät myös arguments-olion. Se sisältää funktiolle annetut argumentit.

Mikäli näkyvyysalueen sisällä pyritään pääsemään käsiksi esimerkiksi foo:n arvoon JavaScript käyttäytyy seuraavasti:

  1. Mikäli var foo-lauseke löytyy tämänhetkisestä näkyvyysalueesta, käytä sen arvoa.
  2. Mikäli eräs funktion parametreista on foo, käytä sitä.
  3. Mikäli funktion nimi itsessään on foo, käytä sitä.
  4. Siirry ulompaan näkyvyysalueeseen ja suorita #1 uudelleen.

Nimiavaruudet

Globaalin nimiavaruuden ongelmana voidaan pitää nimitörmäyksiä. JavaScriptissä tätä ongelmaa voidaan kiertää käyttämällä nimettömiä kääreitä.

(function() {
    // "nimiavaruus" itsessään

    window.foo = function() {
        // paljastettu sulkeuma
    };

})(); // suorita funktio heti

Nimettömiä funktioita pidetään lauseina. Jotta niitä voidaan kutsua, tulee ne suorittaa ensin.

( // suorita sulkeiden sisältämä funktio
function() {}
) // ja palauta funktio-olio
() // kutsu suorituksen tulosta

Samaan lopputulokseen voidaan päästä myös hieman eri syntaksia käyttäen.

// Kaksi muuta tapaa
+function(){}();
(function(){}());

Yhteenveto

On suositeltavaa käyttää nimettömiä kääreitä nimiavaruuksina. Sen lisäksi, että se suojelee koodia nimitörmäyksiltä, se tarjoaa keinon jaotella ohjelma paremmin.

Globaalien muuttujien käyttöä pidetään yleisesti huonona tapana. Mikä tahansa niiden käyttö viittaa huonosti kirjoitettuun, virheille alttiiseen ja hankalasti ylläpidettävään koodiin.

Taulukot

Taulukon iterointi ja attribuutit

Vaikka taulukot ovatkin JavaScript-olioita, niiden tapauksessa ei välttämättä kannata käyttää for in loop-luuppia. Pikemminkin tätä tapaa tulee välttää.

for in-luuppi iteroi kaikki prototyyppiketjun sisältämät ominaisuudet. Tämän vuoksi tulee käyttää erityistä hasOwnProperty-metodia, jonka avulla voidaan taata, että käsitellään oikeita ominaisuuksia. Tästä johtuen iteroint on jo lähtökohtaisesti jopa kaksikymmentä kertaa hitaampaa kuin normaalin for-luupin tapauksessa.

Iterointi

Taulukkojen tapauksessa paras suorituskyky voidaan saavuttaa käyttämällä klassista for-luuppia.

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

Edelliseen esimerkkiin liittyy yksi mutta. Listan pituus on tallennettu välimuistiin erikseen käyttämällä l = list.length-lauseketta.

Vaikka length-ominaisuus määritelläänkin taulukossa itsessään, arvon hakeminen sisältää ylimääräisen operaation. Uudehkot JavaScript-ympäristöt saattavat optimoida tämän tapauksen. Tästä ei kuitenkaan ole mitään takeita.

Todellisuudessa välimuistin käytön pois jättäminen voi hidastaa luuppia jopa puolella.

length-ominaisuus

length-ominaisuuden getteri palauttaa yksinkertaisesti taulukon sisältämien alkioiden määrän. Sen setteriä voidaan käyttää taulukon typistämiseen.

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

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

Pituuden pienemmäksi asettaminen typistää taulukkoa. Sen kasvattaminen ei kuitenkaan vaikuta mitenkään.

Yhteenveto

Parhaan suorituskyvyn kannalta on parhainta käyttää tavallista for-luuppia ja tallentaa length-ominaisuus välimuistiin. for in-luupin käyttö taulukon tapauksessa on merkki huonosti kirjoitetusta koodista, joka on altis bugeille ja heikolle suorituskyvylle.

Array-konstruktori

Array-oletuskonstruktorin käytös ei ole lainkaan yksiselitteistä. Tämän vuoksi suositellaankin, että konstruktorin sijasta käytetään literaalinotaatiota [].

[1, 2, 3]; // Tulos: [1, 2, 3]
new Array(1, 2, 3); // Tulos: [1, 2, 3]

[3]; // Tulos: [3]
new Array(3); // Tulos: []
new Array('3') // Tulos: ['3']

Mikäli Array-konstruktorille annetaan vain yksi argumentti ja se on tyypiltään Number, konstruktori palauttaa uuden harvan taulukon, jonka length-attribuutti on asetettu annetun numeron mukaisesti. On tärkeää huomata, että ainoastaan length asetetaan tällä tavoin, todellisia taulukon indeksejä ei alusteta.

var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, indeksiä ei ole alustettu

Tämä on käytännöllistä vain harvoin, kuten merkkijonon toiston tapauksessa. Tällöin voidaan välttää for-luupin käyttämistä.

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

Yhteenveto

Array-konstruktorin käyttöä tulee käyttää niin paljon kuin suinkin mahdollista. Sen sijaan on suositeltavaa käyttää literaalinotaatiota. Literaalit ovat lyhyempiä ja niiden syntaksi on selkeämpi. Tämän lisäksi ne tekevät koodista luettavampaa.

Tyypit

Yhtäsuuruus ja vertailut

JavaScript sisältää kaksi erilaista tapaa, joiden avulla olioiden arvoa voidaan verrata toisiinsa.

Yhtäsuuruusoperaattori

Yhtäsuuruusoperaattori koostuu kahdesta yhtäsuuruusmerkistä: ==

JavaScript tyypittyy heikosti. Tämä tarkoittaa sitä, että yhtäsuuruusoperaattori muuttaa tyyppejä verratakseen niitä keskenään.

""           ==   "0"           // epätosi
0            ==   ""            // tosi
0            ==   "0"           // tosi
false        ==   "false"       // epätosi
false        ==   "0"           // tosi
false        ==   undefined     // epätosi
false        ==   null          // epätosi
null         ==   undefined     // tosi
" \t\r\n"    ==   0             // tosi

Yllä oleva taulukko näyttää tyyppimuunnoksen tulokset. Tämä onkin eräs pääsyistä, minkä vuoksi ==-operaattorin käyttöä pidetään huonona asiana. Sen käyttö johtaa hankalasti löydettäviin bugeihin monimutkaisista muunnossäännöistä johtuen.

Tämän lisäksi tyyppimuunnos vaikuttaa suorituskykyyn. Esimerkiksi merkkijono tulee muuttaa numeroksi ennen kuin sitä voidaan verrata toiseen numeroon.

Tiukka yhtäsuuruusoperaattori

Tiukka yhtäsuuruusoperaattori koostuu kolmesta yhtäsuuruusmerkistä: ===

Se toimii aivan kuten normaali yhtäsuuruusoperaattori. Se ei tosin tee minkäänlaista tyyppimuunnosta ennen vertailua.

""           ===   "0"           // epätosi
0            ===   ""            // epätosi
0            ===   "0"           // epätosi
false        ===   "false"       // epätosi
false        ===   "0"           // epätosi
false        ===   undefined     // epätosi
false        ===   null          // epätosi
null         ===   undefined     // epätosi
" \t\r\n"    ===   0             // epätosi

Yllä olevat tulokset ovat huomattavasti selkeämpiä ja mahdollistavat koodin menemisen rikki ajoissa. Tämä kovettaa koodia ja tarjoaa myös parempaa suorituskykyä siinä tapauksessa, että operandit ovat erityyppisiä.

Olioiden vertailu

Vaikka sekä == ja === ovat yhtäsuuruusoperaattoreita, ne toimivat eri tavoin, kun ainakin yksi operandeista sattuu olemaan Object.

{} === {};                   // epätosi
new String('foo') === 'foo'; // epätosi
new Number(10) === 10;       // epätosi
var foo = {};
foo === foo;                 // tosi

Tässä tapauksessa molemmat operaattorit vertaavat olion identiteettiä eikä sen arvoa. Tämä tarkoittaa sitä, että vertailu tehdään olion instanssin tasolla aivan, kuten Pythonin is-operaattorin tai C:n osoitinvertailun tapauksessa.

Yhteenveto

On erittäin suositeltavaa, että ainoastaan tiukkaa yhtäsuuruusoperaattoria käytetään. Mikäli tyyppejä tulee muuttaa, tämä kannattaa tehdä selvästi sen sijaan että luottaisi kielen monimutkaisiin muunnossääntöihin.

typeof-operaattori

typeof-operaattori, kuten myös instanceof, on kenties JavaScriptin suurin suunnitteluvirhe. Tämä johtuu siitä, että nämä ominaisuudet ovat liki kokonaan käyttökelvottomia.

Vaikka instanceof-operaattorilla onkin tiettyjä rajattuja käyttötarkoituksia, typeof-operaattorille on olemassa vain yksi käytännöllinen käyttötapaus, joka ei tapahdu olion tyyppiä tarkasteltaessa.

JavaScriptin tyyppitaulukko

Arvo                Luokka     Tyyppi
-------------------------------------
"foo"               String     string
new String("foo")   String     object
1.2                 Number     number
new Number(1.2)     Number     object
true                Boolean    boolean
new Boolean(true)   Boolean    object
new Date()          Date       object
new Error()         Error      object
[1,2,3]             Array      object
new Array(1, 2, 3)  Array      object
new Function("")    Function   function
/abc/g              RegExp     object (Nitro/V8-funktio)
new RegExp("meow")  RegExp     object (Nitro/V8-funktio)
{}                  Object     object
new Object()        Object     object

Yllä olevassa taulukossa Tyyppi viittaa arvoon, jonka typeof-operaattori palauttaa. Kuten voidaan havaita, tämä arvo voi olla varsin ristiriitainen.

Luokka viittaa olion sisäisen [[Luokka]]-ominaisuuden arvoon.

Jotta kyseiseen arvoon päästään käsiksi, tulee soveltaa Object.prototype-ominaisuuden toString-metodia.

Olion luokka

Määritelmä antaa tarkalleen yhden keinon, jonka avulla [[Luokka]] arvoon voidaan päästä käsiksi. Tämä on mahdollista Object.prototype.toString-metodia käyttäen.

function is(type, obj) {
    var clas = Object.prototype.toString.call(obj).slice(8, -1);
    return obj !== undefined && obj !== null && clas === type;
}

is('String', 'test'); // tosi
is('String', new String('test')); // tosi

Yllä olevassa esimerkissä Object.prototype.toString-metodia kutsutaan arvolla this, jonka arvo on asetettu olion [[Luokka]] arvoon.

Määrittelemättömien muuttujien testaaminen

typeof foo !== 'undefined'

Yllä oleva testi kertoo onko foo määritelty. Pelkästään siihen viittaaminen palauttaisi ReferenceError-virheen. Tämä on ainut asia, johon typeof-operaattoria kannattaa käyttää.

Yhteenveto

Ainut tapa, jonka avulla olion tyyppi voidaan tarkistaa luotettavasti, on Object.prototype.toString-metodin käyttö, kuten yllä. Kuten yllä oleva tyyppitaulu näyttää, osa typeof-operaattorin palautusarvoista on huonosti määritelty. Tästä johtuen ne voivat erota toteutuksesta riippuen.

Muuttujan määrittelemättömyyden testaaminen on ainut tapaus, jossa typeof-operaattoria kannattaa käyttää. Muutoin sen käyttöä kannattaa välttää hinnalla milla hyvänsä.

instanceof-operaattori

instanceof-operaattori vertaa kahden operandinsa konstruktoreita keskenään. Se on hyödyllinen ainoastaan, kun vertaillaan itsetehtyjä olioita. Natiivien tyyppien tapauksessa se on lähes yhtä hyödytön kuin typeof-operaattori.

Itsetehtyjen olioiden vertailu

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

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

// Tämä asettaa vain Bar.prototype-ominaisuudeksi
// funktio-olion Foo
// Se ei kuitenkaan ole Foon todellinen instanssi
Bar.prototype = Foo;
new Bar() instanceof Foo; // epätosi

instanceof ja natiivit tyypit

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

'foo' instanceof String; // epätosi
'foo' instanceof Object; // epätosi

On tärkeää huomata, että instanceof ei toimi olioilla, jotka tulevat muista JavaScript-konteksteista (esim. selaimen eri dokumenteista). Tässä tapauksessa niiden konstruktorit viittaavat eri olioon.

Yhteenveto

instanceof-operaattoria tulee käyttää ainoastaan, mikäli käsitellään itsetehtyjä olioita saman JavaScript-kontekstin sisällä. Kuten typeof-operaattorikin, myös muita sen käyttöjä tulee välttää.

Tyyppimuunnokset

JavaScript on tyypitetty heikosti. Tämä tarkoittaa sitä, että se pyrkii pakottamaan tyyppejä aina kun se on mahdollista.

// Nämä ovat totta
new Number(10) == 10; // Number.toString() muutetaan
                      // takaisin numeroksi

10 == '10';           // Merkkijonot muutetaan Number-tyyppiin
10 == '+10 ';         // Lisää merkkijonohauskuutta
10 == '010';          // Ja lisää
isNaN(null) == false; // null muuttuu nollaksi,
                      // joka ei ole NaN

// Nämä ovat epätosia
10 == 010;
10 == '-10';

Yllä havaittu käytös voidaan välttää käyttämällä tiukkaa vertailuoperaattoria. Sen käyttöä suositellaan lämpimästi. Vaikka se välttääkin useita yleisiä ongelma, sisältää se omat ongelmansa, jotka johtavat juurensa JavaScriptin heikkoon tyypitykseen.

Natiivien tyyppien konstruktorit

Natiivien tyyppien, kuten Number tai String, konstruktorit käyttäytyvät eri tavoin new-avainsanan kanssa ja ilman.

new Number(10) === 10;     // Epätosi, Object ja Number
Number(10) === 10;         // Tosi, Number ja Number
new Number(10) + 0 === 10; // Tosi, johtuu tyyppimuunnoksesta

Number-tyypin kaltaisen natiivityypin käyttäminen luo uuden Number-olion. new-avainsanan pois jättäminen tekee Number-funktiosta pikemminkin muuntimen.

Tämän lisäksi literaalit tai ei-oliomaiset arvot johtavat edelleen uusiin tyyppimuunnoksiin.

Paras tapa suorittaa tyyppimuunnoksia on tehdä niitä selvästi.

Muunnos merkkijonoksi

'' + 10 === '10'; // tosi

Arvo voidaan muuttaa merkkijonoksi helposti lisäämällä sen eteen tyhjä merkkijono.

Muunnos numeroksi

+'10' === 10; // tosi

Unaarinen plus-operaattori mahdollistaa numeroksi muuttamisen.

Muunnos totuusarvoksi

Arvo voidaan muuttaa totuusarvoksi käyttämällä not-operaattoria kahdesti.

!!'foo';   // tosi
!!'';      // epätosi
!!'0';     // tosi
!!'1';     // tosi
!!'-1'     // tosi
!!{};      // tosi
!!true;    // tosi

Ydin

Miksi eval-funktiota tulee välttää

eval suorittaa JavaScript-koodia sisältävän merkkijonon paikallisessa näkyvyysalueessa.

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

eval suoritetaan paikallisessa näkyvyysalueessa ainoastaan kun sitä kutsutaan suorasti ja kutsutun funktion nimi on todellisuudessa eval.

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

eval-funktion käyttöä tulee välttää ehdottomasti. 99.9% sen "käyttötapauksista" voidaan toteuttaa ilman sitä.

Piilotettu eval

Aikakatkaisufunktiot setTimeout and setInterval voivat kumpikin ottaa merkkijonon ensimmäisenä argumenttinaan. Kyseinen merkkijono suoritetaan aina globaalissa näkyvyysalueessa, koska tuolloin eval-funktiota kutsutaan epäsuorasti.

Turvallisuusongelmat

eval on myös turvallisuusongelma. Se suorittaa minkä tahansa sille annetun koodin. Tämän vuoksi sitä ei tule ikinä käyttää tuntemattomasta tai epäluotttavasta lähteestä tulevien merkkijonojen kanssa.

Yhteenveto

eval-funktiota ei pitäisi käyttää koskaan. Mikä tahansa sitä käyttävä koodi on kyseenalaista sekä suorituskyvyn että turvallisuuden suhteen. Mikäli jokin tarvitsee eval-funktiota toimiakseen, tulee sen suunnittelutapa kyseenalaistaa. Tässä tapauksessa on parempi suunnitella toisin ja välttää eval-funktion käyttöä.

undefined ja null

JavaScript sisältää kaksi erillistä arvoa ei millekään. Näistä hyödyllisempti on undefined.

undefined ja sen arvo

undefined on tyyppi, jolla on vain yksi arvo: undefined.

Kieli määrittelee myös globaalin muuttujan, jonka arvo on undefined. Myös tätä arvoa kutsutaan nimellä undefined. Tämä muuttuja ei kuitenkaan ole vakio eikä kielen avainsana. Tämä tarkoittaa siis sitä, että sen arvo voidaan ylikirjoittaa.

Seuraavat tapaukset palauttavat undefined-arvon:

  • Globaalin (muokkaamattoman) muuttujan undefined arvon haku.
  • Puuttuvista return-lauseista seuraavat epäsuorat palautusarvot.
  • return-lauseet, jotka eivät palauta selvästi mitään.
  • Olemattomien ominaisuuksien haut.
  • Funktioparametrit, joiden arvoa ei ole asetettu.
  • Mikä tahansa, joka on asetettu arvoon undefined.

Arvon undefined muutosten hallinta

Koska globaali muuttuja undefined sisältää ainoastaan todellisen undefined-tyypin arvon kopion, ei sen asettamienn uudelleen muuta tyypin undefined arvoa.

Kuitenkin, jotta undefined-tyypin arvoa voidaan verrata, tulee sen arvo voida hakea jotenkin ensin.

Tätä varten käytetään yleisesti seuraavaa tekniikkaa. Ajatuksena on antaa itse arvo käyttäen nimetöntä käärettä.

var undefined = 123;
(function(something, foo, undefined) {
    // paikallisen näkyvyysalueen undefined 
    // voi viitata jälleen todelliseen arvoon

})('Hello World', 42);

Samaan lopputuloksen voidaan päästä myös käyttämällä esittelyä kääreen sisällä.

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

})('Hello World', 42);

Tässä tapauksessa ainut ero on se, että pakattu versio vie 4 tavua enemmän tilaa 'var'-lauseen vuoksi.

null ja sen käyttötapaukset

Vaikka undefined-arvoa käytetäänkin usein perinteisen null-arvon sijasta, todellinen null (sekä literaali että tyyppi) on enemmän tai vähemmän vain tietotyyppi.

Sitä käytetään joissain JavaScriptin sisäisissä toiminnoissa, kuten prototyyppiketjun pään toteamisessa (Foo.prototype = null). Useimmissa tapauksissa se voidaan korvata undefined-arvoa käyttäen.

Automaattiset puolipisteet

Vaikka JavaScript käyttääkin C:n tapaista syntaksia, se ei pakota käyttämään puolipisteitä. Niiden käyttöä voidaan halutessa välttää.

Tästä huolimatta JavaScript ei kuitenkaan ole puolipisteetön kieli. Se tarvitsee niitä ymmärtääkseen lähdekoodia. Tämän vuoksi JavaScript-parseri lisää niitä tarpeen mukaan automaattisesti.

var foo = function() {
} // parsimisvirhe, lisätään puolipiste
test()

Lisäys tapahtuu ja parseri yrittää uudelleen.

var foo = function() {
}; // ei virhettä, parsiminen jatkuu
test()

Automaattista puolipisteiden lisäämistä pidetään eräänä JavaScriptin suurimmista suunnitteluvirheistä. Tämä johtuu siitä, että se voi muuttaa tapaa, jolla koodi käyttäytyy.

Kuinka se toimii

Alla oleva koodi ei sisällä puolipisteitä. Täten niiden lisääminen jää parserin tehtäväksi.

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

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

        })

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        )

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

})(window)

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

})(window)

Alla parserin arvaus.

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

        // Not inserted, lines got merged
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- lisätty

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        ); // <- lisätty

        return; // <- lisätty, rikkoo return-lauseen
        { // kohdellaan lohkona

            // nimike ja yhden lausekkeen lause
            foo: function() {} 
        }; // <- lisätty
    }
    window.test = test; // <- lisätty

// Rivit yhdistettiin jälleen
})(window)(function(window) {
    window.someLibrary = {}; // <- lisätty

})(window); //<- lisätty

Yllä olevassa tapauksessa parseri muutti huomattavasti koodin käytöstä. Joissain tapauksissa se tekee kokonaan väärän asian.

Johtavat sulkeet

Parseri ei lisää puolipistettä johtavien sulkeiden tapauksessa.

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

Koodi muuttuu seuraavaksi.

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

On hyvin mahdollista, että log ei palauta funktiota. Tästä johtuen yllä oleva palauttanee TypeError-virheen, joka toteaa että undefined ei ole funktio.

Yhteenveto

On suositeltavaa ettei puolipisteitä jätetä pois milloinkaan. Tämän lisäksi sulut kannattaa pitää niitä vastaavien lausekkeiden kanssa samalla rivillään. if ja else-lauseiden tapauksessa sulkuja kannattaa käyttää aina. Sen lisäksi että edellä mainitut suositukset tekevät koodista johdonmukaisempaa, estävät ne myös JavaScript-parseria muuttamasta sen käytöstapaa.

Muuta

setTimeout ja setInterval

Koska JavaScript on luonteeltaan asynkroninen, voidaan funktioiden suoritusta ajastaa käyttäen setTimeout sekä setInterval-funktioita.

function foo() {}
var id = setTimeout(foo, 1000); // palauttaa Numeron > 0

Kun setTimeout-funktiota kutsutaan, se palauttaa aikakatkaisun tunnisteen ja ajastaa foo-funktion suoritettavaksi suunnilleen tuhannen millisekunnin päästä. foo suoritetaan tarkalleen kerran.

Käytössä olevan JavaScript-tulkin ajastimen tarkkuudesta, JavaScriptin yksisäikeisyydestä sekä muusta koodista riippuen ei ole lainkaan taattua, että viive on tarkalleen sama kuin määritelty.

Ensimmäisenä annettu funktio suoritetaan globaalisti. Tämä tarkoittaa sitä, että sen this on asetettu osoittamaan globaaliin olioon.

function Foo() {
    this.value = 42;
    this.method = function() {
        // this viittaa globaaliin olioon
        console.log(this.value); // tulostaa undefined
    };
    setTimeout(this.method, 500);
}
new Foo();

Kutsujen pinoaminen setInterval-funktion avulla

setTimeout suoritetaan vain kerran. setInterval sen sijaan, kuten nimestä voi päätellä, suoritetaan aina X millisekunnin välein. Sen käyttöä ei kuitenkaan suositella.

Mikäli suoritettava koodi blokkaa katkaisufunktion kutsun, setInterval lisää kutsuja pinoon. Tämä voi olla ongelmallista erityisesti, mikäli käytetään pieniä intervalliarvoja.

function foo(){
    // jotain joka blokkaa sekunnin ajaksi
}
setInterval(foo, 100);

Yllä olevassa koodissa foo-funktiota kutsutaan, jonka jälleen se blokkaa sekunnin ajan.

Tämän ajan aikana setInterval kasvattaa kutsupinon sisältöä. Kun foo on valmis, kutsupinoon on ilmestynyt jo kymmenen uutta kutsua suoritettavaksi.

Mahdollisesti blokkaavan koodin kanssa pärjääminen

Helpoin ja joustavin tapa on käyttää setTimeout-funktiota funktiossa itsessään.

function foo(){
    // jotain joka blokkaa sekunnin ajaksi
    setTimeout(foo, 100);
}
foo();

Sen lisäksi että tämä ratkaisu kapseloi setTimeout-kutsun, se myös estää kutsujen pinoutumisen ja tarjoaa joustavuutta. foo voi päättää halutaanko se suorittaa uudelleen vai ei.

Katkaisujen poistaminen käsin

Katkaisuja ja intervalleja voidaan poistaa antamalla sopiva tunniste joko clearTimeout- tai clearInterval-funktiolle. Se kumpaa käytetään riippuu käytetystä set-funktiosta.

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

Kaikkien katkaisujen poistaminen

JavaScript ei sisällä erityistä funktiota kaikkien katkaisujen ja/tai intervallien poistamiseen. Sen sijaan tämä voidaan toteuttaa raakaa voimaa käyttäen.

// poista "kaikki" katkaisut
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}

On mahdollista, että jopa tämän jälkeen on olemassa katkaisuja, jotka ovat käynnissä. Onkin siis suositeltavaa tallentaa katkaisujen tunnisteet jotenkin. Tällä tavoin ne voidaan poistaa käsin.

Piilotettu eval

setTimeout ja setInterval voivat ottaa myös merkkijonon ensimmäisenä parametrinaan. Tätä ominaisuutta ei tule käyttää ikinä, koska se käyttää sisäisesti eval-funktiota.

function foo() {
    // kutsutaan
}

function bar() {
    function foo() {
        // ei kutsuta ikinä
    }
    setTimeout('foo()', 1000);
}
bar();

Koska eval-funktiota ei kutsuta suoraan, setTimeout-funktiolle annettu merkkijono suoritetaan globaalissa näkyvyysalueessa. Tässä tapauksessa se ei siis käytä paikallista bar-funktion näkyvyysalueessa olevaa foo-funktiota.

Tämän lisäksi on suositeltavaa olla käyttämättä merkkijonoja parametrien antamiseen.

function foo(a, b, c) {}

// Älä käytä tätä IKINÄ
setTimeout('foo(1,2, 3)', 1000)

// Käytä nimetöntä funktiota sen sijaan
setTimeout(function() {
    foo(1, 2, 3);
}, 1000)

Yhteenveto

Merkkijonoa ei tule antaa setTimeout- tai setInterval-funktiolle koskaan. Tämä on selvä merkki erittäin huonosta koodista erityisesti mikäli sitä käytetään parametrien välittämiseen. Sen sijaan kannattaa käyttää nimetöntä funktiota, joka huolehtii varsinaisesta kutsusta.

Tämän lisäksi setInterval-funktion käyttöä tulee välttää. Tämä johtuu siitä, että sen JavaScript ei blokkaa sen vuorottajaa.