JavaScript
- ECMAscript
- Visual Studio Code ja JSHint
- JavaScript-ohjelman suorittaminen
- Työvälineet
- Syntaksi
- Array.sort()
- Tarpeellisia funktioita
Tästä dokumentista löytyy myös vanhentunut interaktiivisempi TIM-versio, mutta parhaiten ajantasalla on tämä sivu.
Käsitellään JavaScript-ohjelmointikielen perusteet. Javascript on ohjelmointikieli jota käytetään erityisesti WWW-sivujen yhteydessä.
Luennolla tehty esimerkki: malli.html ja malli.js
ECMAscript
Javascript on alunperin kehitetty Netscape-selaimeen 1990-luvulla. Javascript-kieli on nykyään standardoitu ECMAScript-standardissa. ECMAScript-kieltä kehitetään edelleen. Tällä hetkellä uusin versio on ECMAScript 2019 mutta käytännössä käytössä on ECMAScript 2015 (ES6).
Voit kirjoittaa Javascriptia uusimmalla kielen versiolla ja kääntää Babelin avulla vanhempaan versioon.
Javascript (ECMAScript) -kieltä käytetään useissa eri yhteyksissä, mutta erityisesti WWW-selaimissa. Selaimessa suoritetulla Javascriptilla voi muokata selainympäristöä ja siihen liittyviä objekteja.
- Javascript ei liity millään tavalla Java-ohjelmointikieleen
- Javascriptissä ei käytetä vahvaa tyypitystä vaan tyyppi päätellään suorituksen aikana. Jos haluat vahvan tyypityksen niin kts. TypeScript tai JSDoc-kommentteja.
- Javascript on prototyyppipohjainen kieli eikä luokkapohjainen kuten esim. Java
- Javascript tyyliopas
Visual Studio Code ja JSHint
Hyvä editori Javascriptin kirjoittamiseen on Visual Studio Code. Huom. tämä on eri asia kuin Visual Studio.
Visual Studio Coden liveserver-ominaisuudesta voi olla paljon apua.
JSHint on työkalu, joka automaattisesti tarkistaa javascript-koodin laatua ja virheitä. JSHintin voi asentaa laajennoksena Visual Studio codeen.
Valitettavasti tämä ei vielä riitä vaan varsinainen JSHint pitää asentaa npm:n avulla joko omalle koneelle tai lokaalisti siihen kansioon, joka on Visual Studio Codessa avoinna. Npm on asennustyökalu, joka tulee nodejs:n mukana. Nodejs on ympäristö, jolla voi suorittaa javascriptia muuallakin kuin selaimessa. Node.js ei liity tämän kurssin aiheisiin, joten emme mene sen käyttämiseen tarkemmin. Yliopiston mikroilla ei nodea ole asennettuna, joten JSHintin asentaminen on hieman hankalaa. Jalava- ja Halava-palvelimista node ja npm löytyvät, mutta ne ovat hieman vanhat versiot.
Tein paketin, jossa pitäisi olla kaikki JSHintin tarvitsemat kirjastot.
http://appro.mit.jyu.fi/tiea2120/jshint.zip
Purkakaa paketti Visual Studio Codessa avoinna olevan kansiorakenteen juureen. Asentakaa Visual Studio Codeen JSHint-laajennus. Jos kaikki toimii niin Problems-ikkunaan ilmestyy JSHintin ilmoituksia. Paketin mukana on myös .jshintrc-tiedosto, johon olen määritellyt sopivat säädöt JSHintin tarkistuksille. Voitte niitä tarpeen mukaan itse muutella. JSHintin Asetuksia voi määritellä myös Visual Studio Coden settingsien kautta.
JavaScript-ohjelman suorittaminen
Javascript-ohjelmat suoritetaan yleensä selainympäristössä. Tämä tarkoittaa javascript-sovelluksen linkittämistä WWW-sivun yhteyteen. Javascript-ohjelma pääsee käsiksi kyseisen WWW-sivun sisältöön ja sen ulkoasuun.
Javascript liitetään www-sivuun script
-elementillä yleensä head-osassa:
<script type="text/javascript" src="jokutiedosto.js"></script>
Mallipohja:
<!DOCTYPE html>
<html lang="fi">
<head>
<meta charset="UTF-8" />
<title>Mallipohja</title>
<script type="text/javascript" src="malli.js"></script>
</head>
<body>
</body>
</html>
Javascriptia voi harjoitella myös suoraan selaimessa konsolin tai scratchpadin avulla tai JSFiddle- ja jsbin-palveluissa.
console.log-funktiolla voit tulostaa tietoja suoraan selaimen konsoliin.
console.log("testi");
Huom! Objekteja ja taulukoita voi myös tulostaa console.log-funktiolla, mutta näissä ei näy tulostushetkinen tilanne vaan objektit ja taulukot ovat ns. live-objekteja, joista näkyy aina viimeisin tilanne. Jos haluat tulostaa konsoliin juuri sen hetkisen tilan, tee se seuraavasti:
// Tekee objektista tai taulukosta kopion JSON-muodon kautta ja tulostaa kopion konsoliin
// ei toimi, jos objektissa/taulukossa on ristiin meneviä viitteitä
console.log(JSON.parse(JSON.stringify(obj));
How you can improve your workflow using the JavaScript console
Luennolla näytetty malli2020.html ja malli2020.js.
Työvälineet
- Visual Studio Code
- Firefox- ja Chrome-www-selaimet
Syntaksi
Javascriptin syntaksista kannattaa lukea seuraavia lähteitä:
Suositeltavaa on suorittaa ohjelma Strict modessa. Kirjoita ohjelmakoodisi alkuun rivi:
"use strict";
Lisää myös seuraava rivi, jos haluat visual studio coden ajavan TypeScriptin tyyppitarkistuksen ohjelmakoodillesi. Koodisi ei tarvitse olla TypeScriptiä. Mahdolliset ongelmat näet Visual Studio Coden problems-ikkunassa View|Problems (Ctrl+Shift+M) Saatat saada myös jotain turhia virheilmoituksia.
//@ts-check
Ympäristö
JavaScriptiä suoritetaan määrätyssä ympäristössä, joka voi olla esimerkiksi selaimessa. JavaScript ei itsessään määrittele, miten tietoa voidaan syöttää tai tulostaa, vaan se tarjoaa ainoastaan tiedon käsittelymekanismit. Tiedon syöttö ja vasteiden anto tehdään käyttäen HTML:ää ja Document Object Model (DOM) -rajapintaa.
Yleistä
- Isoilla ja pienillä kirjaimille on merkitystä (Foo on eri kuin foo)
- Merkistönä käytetään Unicodea
- Rivit päättyvät puolipisteeseen (;). Javascript ei kuitenkaan kaikissa tilanteissa kaipaa puolipistettä (;) rivin loppuun. On erittäin suositeltavaa käyttää puolipistettä aina!
- Kommentit merkitään kuten C++:ssa: // ja /* */
Muuttujat
JavaScript on heikosti tyypitetty kieli. Muuttujien tyyppi määräytyy siis arvon perusteella.
Voit halutessasi käyttää TypeScript-kieltä, joka lisää javascriptiin vahvan tyypityksen. TypeScript-koodi käännetään tavalliseksi Javascriptiksi.
Tietotyypit
- Boolean
- null
- undefined
- Number
- String
- Symbol
- Object
Javascript muuntaa automaattisesti muuttujan tyyppiä tilanteen mukaan.
Muuttujat esitellään seuraavasti let-sanalla, joka esittelee aina lohkon sisäisen muuttujan:
let x = 1.0;
let s = "Diiba daaba";
// vakio, ei voida muuttaa
const foobar = "vakio";
let bar; // määrittelemätön arvo, undefined
if ( bar === undefined ) {
console.log("bar on undefined");
}
if (bar) {
// jos bar on tosi
}
else {
// tämä toteutuu, koska undefined on sama kuin false
}
let luku = bar + 1; // luku saa arvoksi NaN
Muuttujien nimeämissäännöt ovat kuten Javassa.
Javascriptissa on myös var
-sana muuttujien esittelyyn, mutta
älä käytä. var
-sanalla esitellyn muuttujan näkyvyysalue on epäselvä. Näin
esiteltyä muuttujaa voi myös käyttää ennen muuttujan alustamista ja tässä on suuri
riski virheille.
Jos muutat String-tyyppiä Number-tyypiksi niin tee se parseInt-
tai parseFloat-funktiolla.
Kumpikin palauttaa NaN, jos edes merkkijonon alkua ei saa muunnettua numeroksi.
Merkkijonon kelpoisuuden numeroksi voi testata Number(merkkijono)
,
joka palauttaa myös NaN,
jos koko merkkijonoa ei voi muuntaa numeroksi.
Jos haluat erottaa liukuluvut kokonaisluvuista, niin Number.isInteger()
auttaa.
console.log( parseFloat("99") );
99
console.log( parseFloat("99a") );
99
console.log( parseFloat("9a9") );
9
console.log( parseFloat("a99") );
NaN
console.log( parseFloat("99.123") );
99.123
console.log( Number("99") );
99
console.log( Number("9a9") );
NaN
console.log( Number("99a") );
NaN
console.log( Number("a99") );
NaN
console.log( Number("99.123") );
99.123
kts. myös isInteger ja isFinite
Huomioi seuraavaa:
- Liukulukujen tarkkuus n.15 desimaalia.
Tulos on pyöristettävä sopivaan tarkkuuteen.0.2 + 0.1111 = 0.31110000000000004
- Muuttujalla voi olla myös muutama erikoisarvo:
- NaN - Not-A-Number
isNaN(NaN); // true Nan === NaN // false!
- Infinity ja -Infinity - äärettömyydet
- null - viittaus tyhjään. null on objekti
- undefined - arvoa ei määritelty. Ei ole sama asia kuin null. undefined ei ole objekti.
if ( typeof x === 'undefined')
if ( x === undefined)
- Three common mistakes in JavaScript
- Numeric literals
- NaN - Not-A-Number
Lohkot
Muuttujan näkyvyys rajoittuu funktion sisälle, jos muuttuja esitellään var-etuliitteellä. Ennen ECMAScript 2015:sta ja let-muuttujanesittelyä Javascriptissa EI ole ollut lohkon sisäisiä muuttujia. Nyt on käytettävissä let. Käytä AINA let.
function foobar() {
var p = 1; // tämä muuttuja on voimassa vain tässä funktiossa
{
var p = 2; // Tällä on sama näkyvyysalue kuin edellisellä
}
console.log(p); // tulostaa 2
}
function foobarbar() {
let p = 1; // tämä muuttuja on voimassa vain tässä funktiossa
{
let p = 2; // Tämä muuttuja on voimassa vain tässä lohkossa
}
console.log(p); // tulostaa 1
}
Operaattorit
Käytössä on normaalit aritmeettiset ja vertailuoperaattorit. Vrt. Java
- 0, tyhjä merkkijono, NaN, null ja undefined ovat loogisissa operaatioissa totuusarvoltaan false, kaikki muut true
- !! antaa muuttujan totuusarvon
- Matemaattisia operaatioita varten on olemassa globaali objekti Math
- === tarkistaa ovatko saman arvoiset ja samaa tyyppiä. Käytä tätä jos yrität tutkia onko jokin null tai undefined.
=== ja !== operaattorit eivät muunna vertailtavana olevia objekteja mitenkään. Tavallinen == yrittää muuntaa
objektit samantyyppisiksi.
1 === "1" // epätosi 1 == "1" // tosi
delete-operaattori poistaa objektin, ominaisuuden tai elementin taulukosta.
typeof-operaattori palauttaa merkkijonon, joka kertoo kohteena olleen objektin tyypin.
in-operaattori palauttaa true jos kysytty ominaisuus löytyy kohteena olevasta objektista.
instanceof-operaattorilla voi testata objektin tyypin
new-operaattorilla voi luoda uusia objekteja
Merkkijonot
Merkkijonot ovat olioita, joilla on mm. seuraavia metodeja:
Javascriptin merkkijonoja ei voi muuttaa. Merkkijonot ovat immutable. Kaikki merkkijono-operaatiot palauttavat aina uuden merkkijonon.
Merkkijonoja voi operoida seuraavilla operaattoreilla:
- +
- Liittää merkkijonot yhteen
- < > <= >= ==
- Vertailee aakkosjärjestystä
- s[n]
- Antaa merkkijonon s n:nnen kirjaimen
Merkkijonoja voi siis käsitellä kuin taulukoita eli merkkijono.charAt(0) on sama kuin merkkijono[0]
merkkijonoja voidaan javascriptissa yhdistellä yksinkertaisesti +-operaattorilla
let foo = "tämä" + "tämä"
Myös taulukosta voi helposti tehdä merkkijonon ja käyttää haluttua erotinmerkkiä
let foo = ["teksti1","teksti2", "teksti3"]
console.log( foo.join(";") );
Merkkijonojen tyyppien muunnoksiin on olemassa muutama globaali funktio:
let luku = parseInt("567");
let luku = parseInt("567", 10); // toinen argumentti kertoo kantaluvun. Tässä kymmenjärjestelmä
let luku = parseInt("0567"); // ei toimi kuten voisi kuvitella. etunolla tarkoittaa, että luku saatetaan käsitellä oktaalijärjestelmässä.
let luku = parseInt("0567", 10); // tämä toimii
let luku = parseInt("101", 2); // binääriluku
Ehtolauseet
if...else:
if (ehto) {
jos oli tosi...
}
else {
jos oli epätosi...
}
switch
switch (expression) {
case label_1:
statements_1
// break;
case label_2:
statements_2
// break;
// ...
default:
statements_def
}
Silmukat
for- ja while-silmukat toimivat kuten Javassa.
Labelilla voi nimetä tietyn loopin jolloin voi break- tai continue-komennolla viitata tähän looppiin.
ulompi:
while(true) {
console.log('ulompi silmukka');
let x = 0;
while(true) {
console.log('sisempi...');
x = x + 1;
if ( x > 10) {
break ulompi;
}
}
}
for..in ja for..of
for..in ja for..of toimivat hieman eri tavoilla.
let taul = [3, 5, 7];
taul.foo = 'kukkuu';
for (let i in taul) {
// huomaa, että i on merkkijono!
console.log(i); // "0", "1", "2", "foo"
}
// älä käytä muuttujan nimenä tässä yhteydessä i
for (let luku of taul) {
console.log(luku); // 3, 5, 7
}
for..in käy läpi tässä läpi objektin avaimet eli alkioiden paikkanumerot taulukossa ja
myös itse määriteltyjen attribuuttien nimet. Tuloksena saadut avaimet ovat merkkijonoja eivätkä numeroita.
for..in on tarkoitettu objektien läpikäymiseen eikä taulukoiden käsittelyyn.
for..of käy läpi oikeat taulukon arvot.
Käytä aina for..of
-silmukkaa.. Älä käytä for..in
-silmukkaa ellet
varmasti tiedä, että juuri sen käyttäminen on tarpeellista.
Poikkeukset
try {
kokeillaan jotain ...
// throw 'no ny sekos';
}
catch (e) {
tuli poikkeus...
console.log(e);
}
Promise
Käytetään asynkronisten funktioiden yhteydessä. Tähän palataan TIES4080-kurssilla.
Taulukot
Taulukko luodaan seuraavasti:
let paikat = [];
paikat[0] = "Jämsä";
paikat[1] = "Äänekoski";
tai:
let paikat = ["Jämsä", "Äänekoski"];
- Taulukon indeksinä on aina numero.
- Taulukon viimeistä seuraavan indeksin saa taulukko.length -metodilla.
- Huom.
length on nyt 101 eikä 3!paikat[100] = "Helsinki";
Taulukkoon lisääminen onnistuu seuraavasti:
paikat[paikat.length] = "Jyväskylä";
paikat.push("Jyväskylä");
Taulukko on kuten Object, mutta taulukolle on määritelty length
Taulukon kopioiminen
Taulukot ovat oikeasti osoittimia. Taulukkoa ei voi kopioida pelkällä sijoitusoperaatiolla. kts. Array.from ja slice, jotka tekevät shallow-kopion taulukosta. Jos haluaa oikean kopion, niin kopiointi on ohjelmoitava itse.
let mallitaulukko = [
{"foo": "bar"},
{"foo": "foo"},
{"foo": "foobar"},
{"foo": "barfoo"}
];
let kopio = [];
// kaikki seuraavat tekevät shallow-kopion edellisestä taulukosta
for (let malli of mallitaulukko) {
kopio.push(malli);
}
// tai toinen vaihtoehto
kopio = Array.from(mallitaulukko);
// tai
kopio =[...mallitaulukko];
// tai
kopio = mallitaulukko.slice();
// seuraava on väärin EIKÄ tee kopiota taulukosta
// tämä kopioi vain saman taulukon viitteen toiseen muuttujaan
// kopio = mallitaulukko;
// sort muuttaa suoraan kyseistä taulukkoa eli kopio järjestyy uudelleen:
kopio.sort( (a, b) => a.foo.localeCompare(b.foo, 'fi', {sensitivity: 'base'}));
console.log("malli", mallitaulukko);
console.log("kopio", kopio);
//mallitaulukko: [{"foo":"bar"},{"foo":"foo"},{"foo":"foobar"},{"foo":"barfoo"}]
//kopio: [{"foo":"bar"},{"foo":"barfoo"},{"foo":"foo"},{"foo":"foobar"}]
// koska kopiot ovat shallow, on kopioissa kuitenkin viittaukset
// samoihin objekteihin kuin alkuperäisessä taulukossa
// muutetaan kopion toisen alkion (indeksi 1) sisältöä. Samalla muuttuu mallitaulukon
// viimeisen alkion sisältö. Sortin jälkeen alkiot ovat kopiossa eri järjestyksessä, mutta
// viittaukset alkuperäisiin ovat edelleen olemassa
kopio[1]["foo"] = "testi";
console.log("malli", mallitaulukko);
console.log("kopio", kopio);
//malli [{"foo":"bar"},{"foo":"foo"},{"foo":"foobar"},{"foo":"testi"}]
//kopio [{"foo":"bar"},{"foo":"testi"},{"foo":"foo"},{"foo":"foobar"}]
// tehdään deep copy. Tämä täytyy aina kirjoittaa joka tietorakennetta vastaavaksi
kopio = Array.from(mallitaulukko, function(obj) {
// palautetaan uusi objekti johon kopioidaan vanhan sisältö
return {"foo": obj.foo};
});
// nyt muuttuu vain kopio
kopio[3]["foo"] = "testi2";
console.log("malli", mallitaulukko);
console.log("kopio", kopio);
//malli [{"foo":"bar"},{"foo":"foo"},{"foo":"foobar"},{"foo":"testi"}]
//kopio [{"foo":"bar"},{"foo":"foo"},{"foo":"foobar"},{"foo":"testi2"}]
//jos kopioitava rakenne ei sisällä sisäkkäisiä viitteitä tai komplekseja erikoisobjekteja niin seuraava toimii, mutta voi olla hidas
kopio = JSON.parse(JSON.stringify(mallitaulukko));
Delete vs. splice
Javascript-taulukosta poistaminen tapahtuu splice-metodilla.
On olemassa myös delete-operaattori, joka poistaa ominaisuuksia objekteilta. Taulukon alkioiden yhteydessä tämä tarkoittaa käytännössä taulukon alkion muuttamista undefined-tyypiksi. Taulukon koko tai indeksointi ei muutu, jos käyttää delete-operaattoria. Delete on tarkoitettu objektien ominaisuuksien poistamiseen eikä taulukon alkioiden poistamiseen.
Taulukon alkiot voi helposti yhdistää join()-metodilla.
Set
Set on taulukko johon voi tallentaa vain uniikkeja arvoja.
Map
Map on tietorakenne johon voidaan tallentaa avain-arvo-pareja. Vrt. Python dict
Funktiot
Funktion voi esitellä seuraavasti:
let summaa = function(x,y) {
return (x + y);
}
// tai tällä tavalla
function summaa(x,y) {
return (x + y);
}
function foobar(x,y) {
let p = 1;
// tämä funktio barfoo toimii vain funktion foobar-sisällä
// lohkojen sisällä esitellyille funktioille on käytettävä tätä syntaksia:
let barfoo = function(x) {
// ulomman funktion muuttujat ovat käytettävissä
return x*p;
}
}
// parametreille voidaan antaa oletusarvot
function f(x=0,y=1) {
return x+y;
}
Huomioitavaa:
- Funktiota voi kutsua ylimääräisillä parametreilla. Kaikki funktion saamat parametrit voi käydä läpi arguments-taulukolla.
- Jos funktio ei palauta return-lauseella arvoa on funktion palautusarvo undefined
- Lohkojen tai toisten funktioiden sisällä esitellyt funktiot on esiteltävä muodossa:
let funktionnimi = function(...) {...};
- Arrow functions
Javascriptin funktiokutsuissa on oletuksena käytössä pass by value, paitsi jos parametreina on objekteja tai taulukoita jolloin toimii pass by reference.
Objektit
Assosiatiivisia "taulukoita" (nimi-arvo-pareja) voi tehdä seuraavasti (vrt. Map):
Objektit ovat oikeasti osoittimia. Objektia ei voi kopioida pelkällä sijoitusoperaatiolla. Kts. Object.assign(), joka tekee objektista shallow-kopion.
let lampo = new Object;
lampo["jkl"] = -6;
let saatilat = {
jämsä: "Aurinkoinen",
äänekoski: "Pilvipoutaa"
};
for (let i in saatilat) {
console.log( i + " : " + saatilat[i] );
}
Assosiatiivinen "taulukko" on itseasiassa objekti, jolla on attribuutteja. Tämä voi aiheuttaa jossain tilanteissa hämmennystä. Vrt. Map
JavaScript (joka pohjautuu Ecmascript 3 -standardiin) on prototyyppi-pohjainen kieli ja siinä ei ole luokkia samassa mielessä kuin Java/C++/C#-kielissä vaan pelkästään olioita (objekteja). JavaScriptissä omia olioita voidaan luoda seuraavasti:
// konstruktori-funktio
function Koordinaatti(x,y) {
this.x = x;
this.y = y;
}
// Koordinaatin "public-metodi"
Koordinaatti.prototype.normi = function() {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
}
let piste = new Koordinaatti(1,2);
piste.normi();
Voidaan käyttää myös modernimpaa class-syntaksia:
class Polygon {
constructor(height, width) {
this.name = 'Polygon';
this.height = height;
this.width = width;
}
}
class Square extends Polygon {
constructor(length) {
// super kutsuu Polygonin konstruktoria. Suoritettava ennen kuin voi käyttää this
super(length, length);
this.name = 'Square';
}
}
let poly = new Polygon(10, 10);
let nelio = new Square(20);
Objekteja laajennetaan toisten objektien avulla käyttäen prototype-määritystä. Private-attribuutteja ja -metodeja on mahdollista luoda pienellä vaivalla.
// muistakaa, että attribuutteja (avaimia) voi vapaasti keksiä
let testi = new Object();
undefined
testi.foo;
undefined
testi.foo==undefined
true
testi.foo = "koe";
"koe"
testi.foo==undefined;
false
delete testi.foo;
true
testi.foo==undefined;
true
// muistakaa, että objekteilla testi.bar on sama asia kuin testi["bar"]
testi["bar"] = "koe2";
"koe2"
testi.bar;
"koe2"
testi.bar==undefined;
false
delete testi["bar"];
true
testi.bar==undefined;
true
Array.sort()
Teille on opetettu ohjelmoinnin peruskursseilla miten kirjoitetaan bubblesort. Algoritmikurssilla on varmaankin opetettu miten kirjoitetaan quicksort, radixsort ja ties mitä muita. Nyt olette sillä tasolla, että voitte unohtaa kaikki nämä.
ÄLÄ KOSKAAN KIRJOITA OMAA JÄRJESTÄMISALGORITMIA
Käytä ohjelmointikielen kirjastojen tarjoamia. Joku muu on jo toteuttanut sinua paremmin tehokkaan järjestämisen. Luotatte siihen, että kyseessä on tehokas funktio.
Ongelmaksi jää, että mitenkäs tämä yleinen järjestämisfunktio osaa järjestää kaikkien maailman ohjelmoijien keksimät objektit, luokat sun muut?
Aloitetaan aivan alusta. Javascriptissa järjestäminen toimii hienosti merkkijonoilla:
let taulukko = ["foo", "bar", "foobar"];
taulukko.sort()
Array(3) [ "bar", "foo", "foobar" ]
Entäpä seuraava:
taulukko = [10, 5, 40];
taulukko.sort()
Array(3) [ 10, 40, 5 ]
Väärin meni. Javascriptissa sort()-funktio yrittää muuntaa kaiken merkkijonoiksi. Tässä tapauksessa myös numerot muuntuvat merkkijonoiksi ja ne järjestyvät väärin.
Miten tämä korjataan? Järjestämisessä olennainen kysymys on, että miten laitetaan kaksi järjestettävää alkiota suuruusjärjestykseen. Riippumatta siitä minkälainen järjestettävä objekti/alkio on, niin järjestäminen onnistuu, jos jotenkin voimme määrätä näiden suuruusjärjestyksen.
Javascriptin sort()-funktiolle voidaan antaa oma vertailufunktio. sort() olettaa, että tämä funktio palauttaa 0, jos vertailtavat asiat ovat samansuuruiset. Jos vertailtava a on pienempi kuin b, niin sort() olettaa saavansa negatiivisen luvun. Jos taas vertailtava a onkin suurempi kuin b, niin sort() haluaa positiivisen luvun.
Jos on huolimaton tämän funktion kanssa, niin ampuu helposti itseään jalkaan. Väärin kirjoitettu funktio voi tuurilla toimia firefoxissa, mutta ei toimikaan esim. Chromessa.
Numeroiden vertailu voidaan kirjoittaa siis näin yksinkertaisesti:
function compareNumbers(a, b) {
return a - b;
}
Ja tätä käytetään seuraavalla tavalla:
taulukko.sort(compareNumbers);
Array(3) [ 5, 10, 40 ]
Nyt toimii! Huomatkaa, että sort()-funktiolle annetaan parametrina tuon oman vertailufunktion osoitin eli javascriptissa pelkkä funktion nimi.
- osoitin funktioon:
compareNumbers
- funktiokutsu:
compareNumbers()
Entäpä monimutkaisemmat objektit?
taulukko = [
{
"etunimi": "Oona",
"sukunimi": "Opiskelija"
},
{
"etunimi": "Olli",
"sukunimi": "Opiskelija"
},
{
"etunimi": "Tommi",
"sukunimi": "Lahtonen"
}
];
Nyt taulukossa on objekteja. Javascript ei osaa näitä automaattisesti muuntaa järkeviksi merkkijonoiksi joten taas tarvitaan oma vertailufunktio.
Yleinen aina toimiva vertailufunktion muoto olisi seuraava:
function compare(a, b) {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
// yhtäsuuret
return 0;
}
Tämä ei tietenkään suoraan toimi vaan nyt pitää muuttaa muuttujien a ja b käsittely vastaamaan omia objektejamme eli pitää vertailla sukunimiä:
function compare(a, b) {
if (a.sukunimi < b.sukunimi) {
return -1;
}
if (a.sukunimi > b.sukunimi) {
return 1;
}
// yhtäsuuret
return 0;
}
taulukko.sort(compare);
Array(3) [ {…}, {…}, {…} ]
0: Object { etunimi: "Tommi", sukunimi: "Lahtonen" }
1: Object { etunimi: "Oona", sukunimi: "Opiskelija" }
2: Object { etunimi: "Olli", sukunimi: "Opiskelija" }
Järjestäminen toimii, mutta ei aivan täysin eli pitäisi vertailla myös etunimiä, koska sukunimi voi olla sama. Parannellaan funktiota:
function compare(a, b) {
if (a.sukunimi < b.sukunimi) {
return -1;
}
if (a.sukunimi > b.sukunimi) {
return 1;
}
if (a.etunimi > b.etunimi) {
return -1;
}
if (a.etunimi < b.etunimi) {
return 1;
}
// yhtäsuuret
return 0;
}
taulukko.sort(compare);
Array(3) [ {…}, {…}, {…} ]
0: Object { etunimi: "Tommi", sukunimi: "Lahtonen" }
1: Object { etunimi: "Olli", sukunimi: "Opiskelija" }
2: Object { etunimi: "Oona", sukunimi: "Opiskelija" }
Samaan tapaan toimittaisiin olisi kyseessä ihan mitä tahansa järjestettävää. Vertailufunktion voi kirjoittaa mieleisekseen. Jos haluaakin järjestyksen päinvastaiseksi? Muuttaa palautusarvot 1 ja -1 toistepäin. Entäs jos pienillä ja isoilla kirjaimilla ei saa olla merkitystä? Mitenkäs mahdolliset välilyönnit etunimen tai sukunimen alussa?
Objektin muuntaminen merkkijonoksi
Kuten edellä kerrottiin, niin javascript yrittää sort()-funktion yhteydessä muuntaa kaiken merkkijonoksi ellei käytössä ole omaa vertailufunktiota. Kaikki objektit perivät Object-luokalta toString()-metodin, joka oletuksena muuntaa objektit muotoon:
[object Object]
Tämä metodi voidaan helposti ylimääritellä:
function opisToString() {
return this.sukunimi + ' ' + this.etunimi;
}
taulukko = [
{
"etunimi": "Oona",
"sukunimi": "Opiskelija",
toString: opisToString
},
{
"etunimi": "Olli",
"sukunimi": "Opiskelija",
toString: opisToString
},
{
"etunimi": "Tommi",
"sukunimi": "Lahtonen",
toString: opisToString
}
];
// seuraava toimii nyt ilman vertailufunktiota, joska
// objekti muuntuu järkeväksi vertailukelpoiseksi merkkijonoksi
taulukko.sort();
Array(3) [ {…}, {…}, {…} ]
0: Object { etunimi: "Tommi", sukunimi: "Lahtonen", toString:
opisToString() }
1: Object { etunimi: "Olli", sukunimi: "Opiskelija", toString:
opisToString() }
2: Object { etunimi: "Oona", sukunimi: "Opiskelija", toString:
opisToString() }
Tätä samaa toString()-metodia voi kokeilla myös console.logilla esim. seuraavasti:
for(let opis of taulukko) {
// "" + pakottaa objektin muuntumaan merkkijonoksi
console.log("" + opis);
}
Objektin metodien uudelleen määrittelemiseen on järkevämpiäkin tapoja, kuten seuraava:
class Opiskelija {
constructor(etunimi, sukunimi) {
this.etunimi = etunimi;
this.sukunimi = sukunimi;
}
// määritellään oma toString-metodi
toString() {
return this.sukunimi + " " + this.etunimi;
}
}
let taulukko = [
new Opiskelija("Oona", "Opiskelija"),
new Opiskelija("Ölli", "Opiskelija"),
new Opiskelija("Tommi", "Lahtonen")
]
taulukko.sort()
console.log(taulukko);
//[{"etunimi":"Tommi","sukunimi":"Lahtonen"},{"etunimi":"Oona","sukunimi":"Opiskelija"},{"etunimi":"Ölli","sukunimi":"Opiskelija"}]
// pakotetaan merkkijonoksi niin saadaan suoraan nätti muoto
console.log(""+taulukko);
//Lahtonen Tommi,Opiskelija Oona,Opiskelija Ölli
localeCompare() ja Intl.Collator
Merkkijonojen vertailua helpottaa localeCompare(), joka osaa vertailla merkkijonoja kielikohtaisella tavalla. localeComparen palautusarvot ovat suoraan sortin vertailufunktioksi sopivat. Tässä täytyy kuitenkin olla tarkkana ja käyttää oikeita parametreja eli määrätä kieleksi suomi (fi) ja sensitivityn on oltava "base", jolloin isoilla ja pienillä kirjaimilla ei ole merkitystä. Oletuksena kieli menee käyttäjän selaimen asetusten mukaan ja sensitivity on "variant", joka erottelee isot ja pienet kirjaimet.
Yksinkertaisin vaihtoehto puhtaille merkkijonotaulukoille on käyttää Intl.Collator-objektia, joka palauttaa kielikohtaisen vertailufunktion.
let taulukko = ["b", "a", "ö", "o"];
// helpointa suoraan Intl.Collatorin avulla
taulukko.sort(Intl.Collator('fi', { sensitivity: 'base' }).compare);
// sama localeComparella
function omacompare(a, b) {
return a.localeCompare(b, 'fi', {sensitivity: 'base'})
}
taulukko.sort(omacompare);
// tai sama onnistuu myös seuraavalla tavalla
taulukko.sort( (a, b) => a.localeCompare(b, 'fi', {sensitivity: 'base'}));
// objektit aivan samaan tapaan
taulukko = [
{
"etunimi": "Oona",
"sukunimi": "Opiskelija"
},
{
"etunimi": "Ölli",
"sukunimi": "Opiskelija"
},
{
"etunimi": "Tommi",
"sukunimi": "Lahtonen"
}
];
// objektien vertailu
function compare(a, b) {
let tulos = a.sukunimi.localeCompare(b.sukunimi, 'fi', {sensitivity: 'base'})
// jos sukunimissä oli eroa
if ( tulos ) {
return tulos;
}
// jos sukunimet olivat samat palautetaan suoraan etunimien vertailutulos
return a.etunimi.localeCompare(b.etunimi, 'fi', {sensitivity: 'base'})
}
taulukko.sort(compare);
Lisätietoa:
Tarpeellisia funktioita
Date
Päivämäärä
let aika = new Date();
alert(aika.getDate() + "." + aika.getMonth() + "." + aika.getYear());
Kulunut aika
let aika = new Date();
let hetkiA_msec = aika.getTime();
// tehdään jotain
let hetkiB_msec = aika.getTime();
let kulunutaika = new Date(hetkiB_msec - hetkiA_msec);
alert(kulunutaika.getSeconds());
Math
Satunnaisluvut
let sanoja = ["Diiba","Daaba","Tesmaus","Heppa","Muuli","Saippuakauppias","Jepjep"];
// pyöristys alaspäin( 0-0.999999999 * 7 )
let rand = Math.floor( Math.random() * sanoja.length );
console.log(sanoja[rand]);
Pyöristäminen ja numeroiden muotoilu
let liukuluku = 3.245200004;
let kaksidesimaalia = Math.round(liukuluku*100)/100
Numeroiden formatointiin javascriptissa ei ole tarjolla esim. c-kielestä tuttua sprintf/printf-mahdollisuutta.
Numeroiden muotoilu on helpointa tehdä Intl.NumberFormat-avulla.
// tulostaa etunollan numeroon:
let myf = new Intl.NumberFormat('fi-FI', {minimumIntegerDigits: 2});
myf.format(5);
isNaN
Tutkii onko muuttuja erikoisarvoa NaN (not a number). NaN-arvoa ei voi vertailla muulla kuin tällä funktiolla.
let numero = parseInt(document.getElementById("tekstikentta").value);
if (isNaN(numero))
document.getElementById("tekstikentta").className = "red";
typeof
Operaattorit: typeof - Core JavaScript 1.5 Reference
Palauttaa stringinä muuttujan tyypin.
Käyttäjien kommentit