JavaScript

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.

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

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ä

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

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:

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

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

Error types

Promise

Käytetään asynkronisten funktioiden yhteydessä. Tähän palataan TIES4080-kurssilla.

Using promises

Taulukot

Taulukko luodaan seuraavasti:

let paikat = [];
paikat[0] = "Jämsä";
paikat[1] = "Äänekoski";

tai:

let paikat = ["Jämsä", "Äänekoski"];

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:

function summaa(x,y) {
  return (x + y);
}

let summaa = function(x,y) {
  return (x + y);
}

function foobar(x,y) {
  let p = 1;
  // tämä funktio 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:

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.

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

Kommentoi Lisää kommentti
Informaatioteknologia - Jyväskylän yliopiston informaatioteknologian tiedekunta