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).
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).
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.
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
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};
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.
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.
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.
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.
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.
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.
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.
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
.
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
ominaisuutenaJavaScript 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
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
-luuppiAivan 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.
On suositeltavaa käyttää aina hasOwnProperty
-metodia. Ei ole kannattavaa tehdä ajoympäristöön tai prototyyppeihin liittyviä oletuksia.
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äärefunction 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
-lausekevar 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.
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.
this
toimiiJavaScripting this
toimii eri tavoin kuin useimmissa kielissä. Tarkalleen ottaen on olemassa viisi eri tapaa, joiden mukaan sen arvo voi määrittyä.
this;
Kun this
-muuttujaa käytetään globaalissa näkyvyysalueessa, viittaa se globaaliin olioon.
foo();
Tässä tapauksessa this
viittaa jälleen globaaliin olioon.
test.foo();
Tässä esimerkissä this
viittaa test
-olioon.
new foo();
Funktiokutsu, jota edeltää new
-avainsana toimii konstruktorina. Funktion sisällä this
viittaa juuri luotuun Object
-olioon.
this
-arvon asettaminenfunction 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.
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.
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 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.
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.
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
.
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.
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
-olioJokainen 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.
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.
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);
};
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);
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ä.
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.
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ä.
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.
new
-avainsanan käyttöä on vastoin kielen filosofista perustaa.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ä.
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.
// 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ä.
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.
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 = {};
}
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:
var foo
-lauseke löytyy tämänhetkisestä näkyvyysalueesta, käytä sen arvoa.foo
, käytä sitä.foo
, käytä sitä.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(){}());
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.
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.
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
-ominaisuuslength
-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.
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
-konstruktoriArray
-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);
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.
JavaScript sisältää kaksi erilaista tapaa, joiden avulla olioiden arvoa voidaan verrata toisiinsa.
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 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ä.
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.
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
-operaattoritypeof
-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.
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.
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.
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ää.
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
-operaattoriinstanceof
-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.
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 tyypitnew 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.
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ää.
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, 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.
'' + 10 === '10'; // tosi
Arvo voidaan muuttaa merkkijonoksi helposti lisäämällä sen eteen tyhjä merkkijono.
+'10' === 10; // tosi
Unaarinen plus-operaattori mahdollistaa numeroksi muuttamisen.
Arvo voidaan muuttaa totuusarvoksi käyttämällä not-operaattoria kahdesti.
!!'foo'; // tosi
!!''; // epätosi
!!'0'; // tosi
!!'1'; // tosi
!!'-1' // tosi
!!{}; // tosi
!!true; // tosi
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ä.
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.
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.
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 arvoundefined
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:
undefined
arvon haku.return
-lauseista seuraavat epäsuorat palautusarvot.return
-lauseet, jotka eivät palauta selvästi mitään.undefined
.undefined
muutosten hallintaKoska 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ötapauksetVaikka 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.
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.
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.
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
.
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.
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();
setInterval
-funktion avullasetTimeout
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.
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.
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);
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.
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)
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.