JavaScript

Tästä dokumentista löytyy myös vanhentunut interaktiivisempi TIM-versio, mutta parhaiten ajan tasalla on tämä sivu.

Käsitellään JavaScript-ohjelmointikielen perusteet ja tietorakenteet. Tämä sivu korvaa aiemmat erilliset sivut Javascript-perusteet ja Javascript-tietorakenteet.

Javascript on ohjelmointikieli, jota käytetään erityisesti WWW-sivujen yhteydessä.

Luentovideo: JavaScript-kielen perusteet. Jos video ei näy, katso se suoraan YouTubesta.

Luennolla tehty esimerkki: malli.html ja malli.js

ECMAscript

Javascript on alun perin kehitetty Netscape-selaimeen 1990-luvulla. Javascript-kieli on nykyään standardoitu ECMAScript-standardissa (ECMA-262). Standardin kehitystyötä tekee TC39-komitea, ja työn alla oleva versio on aina luettavissa osoitteessa https://tc39.es/ecma262/.

Uusi versio julkaistaan kesäkuussa joka vuosi. Uusin hyväksytty versio on ECMAScript 2026 (17. laitos, hyväksytty 30.6.2026). Isoin yksittäinen uudistus oli mullistava vuoden 2015 versio (ES6 eli ECMAScript 2015), jossa tulivat mm. let, const, nuolifunktiot, luokat, Map, Set, for..of ja template-merkkijonot. Kaikki ylläpidetyt selaimet tukevat nykyään ES2015:tä ja huomattavasti uudempaakin, joten tällä kurssilla kirjoitetaan modernia JavaScriptia eikä varauduta vanhoihin selaimiin.

Jos joskus törmäät projektiin, jonka on toimittava vanhoissa ympäristöissä, voit kirjoittaa uusimmalla kielen versiolla ja kääntää Babelin avulla vanhempaan versioon. Tällä kurssilla sitä ei tarvita.

Selainten ja ympäristöjen ominaisuustuen voi tarkistaa osoitteista caniuse.com ja Baseline / web-features.

Javascript (ECMAScript) -kieltä käytetään useissa eri yhteyksissä, mutta erityisesti WWW-selaimissa. Selaimessa suoritetulla Javascriptilla voi muokata selainympäristöä ja siihen liittyviä objekteja.

Työvälineet

Hyvä editori Javascriptin kirjoittamiseen on Visual Studio Code. Huom. tämä on eri asia kuin Visual Studio.

Visual Studio Coden Live Server -laajennuksesta voi olla paljon apua: se tarjoilee tiedostot oikean WWW-palvelimen kautta ja päivittää selaimen automaattisesti. Tämä on tärkeää, koska osa asioista (mm. moduulit ja fetch) ei toimi lainkaan, jos avaat sivun suoraan tiedostojärjestelmästä file://-osoitteella.

Visual Studio Code on iso ja raskas ohjelma, eikä sitä tarvita kaikkeen: pieniin kokeiluihin riittää selaimen konsoli. Jos et halua Microsoftin telemetriaa, käytä VSCodiumia, joka on samasta lähdekoodista käännetty versio. Myös Neovim, Emacs, Sublime Text ja Zed sopivat javascript-koodaukseen hyvin. Valitse työkalu, jonka osaat, äläkä opettele uutta editoria samaan aikaan uuden kielen kanssa.

Luentovideo: Visual Studio Code ja työvälineet. Jos video ei näy, katso se suoraan YouTubesta.

Selaimen kehittäjätyökalut

Firefoxin ja Chromen kehittäjätyökalut (F12) ovat tärkein työkalusi:

Huom. Firefoxin Scratchpad poistettiin Firefox 72:sta (2020). Sen tilalla ovat konsoli ja kehittäjätyökalujen Snippets-toiminto sekä palvelut kuten JSFiddle ja CodePen.

JSON-datan tutkimiseen kätevä on JSON editor online.

Koodin tarkistus

Käytä ESLintiä. Se on nykyään käytännössä ainoa aktiivisesti kehitetty JavaScript-linter (JSHint on edelleen olemassa, mutta ei tunne uusimpia kielen ominaisuuksia). ESLintin voi asentaa VS Code -laajennuksena, ja se tarvitsee lisäksi projektikansioon konfiguraatiotiedoston eslint.config.js:

import js from "@eslint/js";

export default [
  js.configs.recommended,
  {
    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",
      globals: { window: "readonly", document: "readonly", console: "readonly" }
    }
  }
];

Varsinainen ESLint asennetaan npm:llä (npm init -y ja npm install --save-dev eslint @eslint/js). Npm tulee Node.js:n mukana. Node.js on ympäristö, jolla voi suorittaa javascriptia muuallakin kuin selaimessa; se ei kuulu tämän kurssin aiheisiin, mutta työkalut asennetaan sillä.

Jos npm:n asentaminen ei onnistu (esim. yliopiston mikroluokassa), pelkkä VS Coden sisäänrakennettu tarkistus vie jo pitkälle — katso //@ts-check alla.

//@ts-check

Kirjoita tiedoston alkuun seuraava rivi, jos haluat Visual Studio Coden ajavan TypeScriptin tyyppitarkistuksen ohjelmakoodillesi. Koodisi ei tarvitse olla TypeScriptiä. Mahdolliset ongelmat näet Problems-ikkunassa (View | Problems, Ctrl+Shift+M).

//@ts-check

Tämä löytää yllättävän suuren osan kirjoitusvirheistä ja on ilmainen, koska TypeScript-palvelin tulee VS Coden mukana. Saatat saada myös joitain turhia virheilmoituksia.

Sivun rakenteen tarkistaminen

Selain korjaa rikkinäistä HTML:ää automaattisesti, ja jokainen selain korjaa sitä hieman eri tavalla. Siksi rakennevirhettä ei välttämättä itse huomaa lainkaan: sivu näyttää omalla koneella oikealta, kunnes korjauskyky loppuu tai joku avaa sivun toisella selaimella ja rakenne hajoaa. On siis syytä kirjoittaa rakenteellisesti oikeaa koodia alusta asti eikä luottaa siihen, että lopputulos näyttää oikealta.

Tarkistamiseen on kolme tasoa, ja ne kannattaa ottaa käyttöön tässä järjestyksessä.

1. Firefox XML-tilassa

Yksinkertaisin ja nopein tapa bongata rakennevirheet on nimetä tiedosto .xhtml-päätteellä ja avata se Firefoxilla. Firefox jäsentää tiedoston tällöin oikealla XML-jäsentimellä, joka pysähtyy heti ensimmäiseen muotovirheeseen ja kertoo rivinumeron. Palaute tulee sekunneissa eikä vaadi mitään asennettavaa.

Jotta sama tiedosto toimisi myös HTML-dokumenttina, html-elementin on oltava seuraavaa muotoa:

<html xmlns="http://www.w3.org/1999/xhtml" lang="fi" xml:lang="fi">

xmlns-attribuutti on pakollinen XML-jäsennystä varten, ja lang- sekä xml:lang-attribuuteissa on oltava sama arvo, jotta kumpikin jäsennin ymmärtää kielen samoin.

XML-tila on tiukka, ja juuri se on sen hyöty. Muista näiden lisäksi:

Tiukkuus on tarkoituksellista: kun sivu tarjoillaan tyyppinä application/xhtml+xml, yksikin muotovirhe tuottaa virhesivun sen sijaan, että selain paikkaisi sen hiljaa. Virhe tulee siis näkyviin heti eikä vasta silloin, kun joku avaa sivun toisella selaimella.

2. W3C:n validaattori

W3C:n validaattori (nykyisin Nu Html Checker) tarkistaa hyvinmuodostuneisuuden lisäksi myös sen, ovatko elementit sallituissa paikoissa ja onko pakolliset attribuutit annettu. Firefoxin XML-tila ei näistä välitä, joten työkalut täydentävät toisiaan.

Validaattorille voi antaa osoitteen, ladata tiedoston tai liittää koodin tekstikenttään.

3. Lintteri editorissa

Nopein palaute tulee editorista. Katso yllä oleva luku Koodin tarkistus: ESLint javascript-koodille ja //@ts-check tyyppivirheille.

Yhteenveto

Työkalu Mitä tarkistaa Milloin
ESLint javascript-koodin virheet ja tyyli kirjoittaessa
//@ts-check tyyppivirheet ja kirjoitusvirheet kirjoittaessa
Firefox + .xhtml HTML:n hyvinmuodostuneisuus sivua avatessa
W3C-validaattori HTML:n rakenteen oikeellisuus ennen palautusta
Kehittäjätyökalujen debugger ohjelman logiikka kun jokin ei toimi

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ä:

<script src="jokutiedosto.js"></script>

Mallipohja:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="fi" xml:lang="fi">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Mallipohja</title>
    <script src="malli.js"></script>
  </head>
  <body>
  </body>
</html>

Huomioitavaa:

Strict mode

Suositeltavaa on suorittaa ohjelma strict modessa. Kirjoita ohjelmakoodisi alkuun rivi:

"use strict";

Strict mode muuttaa hiljaiset virheet oikeiksi virheilmoituksiksi (esim. arvon sijoittaminen esittelemättömään muuttujaan). ES-moduuleissa (type="module") strict mode on aina päällä automaattisesti, joten silloin riviä ei tarvita.

Konsoliin tulostaminen

console.log-funktiolla voit tulostaa tietoja suoraan selaimen konsoliin.

console.log("testi");
console.log("nimi on", nimi, "ja ikä", ika);   // pilkuilla eroteltuna, ei +

Huom! Objekteja ja taulukoita voi myös tulostaa console.log-funktiolla, mutta konsolissa ne ovat ns. live-objekteja: kun avaat kolmion, näet objektin nykyisen tilan etkä sitä, millainen se oli tulostushetkellä. Jos haluat tilannekuvan juuri tulostushetkeltä, tee kopio:

// structuredClone on tähän paras: se osaa myös ristiin menevät viitteet
console.log(structuredClone(obj));

// vanha tapa JSON-muodon kautta:
// ei toimi, jos objektissa/taulukossa on ristiin meneviä viitteitä
console.log(JSON.parse(JSON.stringify(obj)));

Hyödyllisiä muitakin: console.table(taulukko) piirtää taulukkomuotoisen datan taulukkona, console.error ja console.warn korostavat viestin, ja console.dir(elementti) näyttää DOM-elementin objektina eikä HTML:nä.

How you can improve your workflow using the JavaScript console

Luennolla näytetty malli2020.html ja malli2020.js.

Syntaksi

Javascriptin syntaksista kannattaa lukea seuraavia lähteitä:

Ympäristö

JavaScriptiä suoritetaan määrätyssä ympäristössä, joka voi olla esimerkiksi selain. 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

Muuttujat esitellään const- tai let-sanalla. Kumpikin esittelee lohkon sisäisen muuttujan.

const nimi = "Diiba daaba";   // ei voi sijoittaa uudelleen
let laskuri = 0;              // arvoa voi muuttaa
laskuri = laskuri + 1;

let bar;                      // arvoa ei annettu, arvo on undefined

if (bar === undefined) {
  console.log("bar on undefined");
}

if (bar) {
  // ei toteudu
}
else {
  // tämä toteutuu, koska undefined on epätosi (falsy) totuusarvokontekstissa
}

const luku = bar + 1;         // luku saa arvoksi NaN

Käytä oletuksena const ja vaihda let-sanaan vasta, kun huomaat tarvitsevasi uudelleensijoitusta. Näin lukija näkee yhdellä silmäyksellä, mikä muuttuu ja mikä ei.

Huomaa, että const estää vain uudelleensijoituksen, ei objektin sisällön muuttamista:

const lista = [1, 2, 3];
lista.push(4);        // OK! taulukon sisältöä saa muuttaa
lista = [5, 6];       // TypeError: Assignment to constant variable.

Jos haluat estää myös sisällön muuttamisen, käytä Object.freeze().

Javascriptissa on myös var-sana muuttujien esittelyyn, mutta älä käytä sitä. var-sanalla esitellyn muuttujan näkyvyysalue on koko funktio lohkosta riippumatta, ja muuttujaa voi käyttää jo ennen esittelyriviä (arvo on tällöin undefined). Tässä on suuri riski virheille.

Muuttujien nimeämissäännöt ovat kuten Javassa.

Lohkot

Muuttujan näkyvyys rajoittuu funktion sisälle, jos muuttuja esitellään var-etuliitteellä. Ennen ECMAScript 2015:tä Javascriptissa ei ole ollut lohkon sisäisiä muuttujia lainkaan. Nyt on käytettävissä let ja const.

function foobar() {
  var p = 1;      // tämä muuttuja on voimassa koko funktiossa
  {
    var p = 2;    // tällä on sama näkyvyysalue kuin edellisellä
  }
  console.log(p); // tulostaa 2
}

function foobarbar() {
  let p = 1;      // voimassa koko funktiossa
  {
    let p = 2;    // tämä muuttuja on voimassa vain tässä lohkossa
  }
  console.log(p); // tulostaa 1
}

Huomaa, että saman muuttujan esittely kahdesti samassa lohkossa on let- ja const-sanoilla virhe:

let luku = 1;
let luku = 2;   // SyntaxError: Identifier 'luku' has already been declared

Tämä on hyvä asia: var olisi sallinut sen hiljaisesti.

Tietotyypit

Javascriptissä on seitsemän primitiivistä tyyppiä sekä objektit:

Tyyppi Esimerkki Huomioita
Boolean true, false
Number 42, 3.14 liukuluku (IEEE 754 double)
BigInt 9007199254740993n mielivaltaisen suuret kokonaisluvut
String "teksti" muuttumaton (immutable)
Symbol Symbol("kuvaus") uniikki tunniste; harvoin tarpeen perusohjelmoinnissa
null null “tarkoituksellisesti tyhjä”
undefined undefined “arvoa ei ole asetettu”
Object {}, [], funktiot kaikki muu

Javascript muuntaa automaattisesti arvon tyyppiä tilanteen mukaan.

null ja undefined

let a;                    // undefined: arvoa ei ole koskaan asetettu
const b = null;           // null: ohjelmoija on tarkoituksella merkinnyt tyhjäksi

console.log(a === b);     // false, eri arvoja
console.log(a == b);      // true (== pitää näitä samanarvoisina)

console.log(typeof a);    // "undefined"
console.log(typeof b);    // "object"  <-- HUOM!

typeof null palauttaa "object", mutta null ei ole objekti — se on primitiiviarvo. Kyseessä on kielen tunnettu historiallinen bugi ensimmäisestä toteutuksesta vuodelta 1995, jota ei voi enää korjata rikkomatta olemassa olevia sivustoja. Älä siis testaa null-arvoa typeof-operaattorilla vaan suoraan:

if (arvo === null) { ... }
if (arvo === undefined) { ... }
if (arvo === null || arvo === undefined) { ... }   // tai lyhyemmin:
if (arvo == null) { ... }                          // == null kattaa molemmat

Numerot ja liukuluvut

Kaikki tavalliset Javascriptin numerot ovat 64-bittisiä liukulukuja, myös “kokonaisluvut”. Tästä seuraa kaksi asiaa, jotka tulee tietää:

1. Desimaaliluvut eivät ole tarkkoja. Luvut 0.1 ja 0.2 eivät ole esitettävissä tarkasti binäärimuodossa, aivan kuten 1/3 ei ole esitettävissä tarkasti desimaalimuodossa.

console.log(0.2 + 0.1111);       // 0.31110000000000004
console.log(0.1 + 0.2 === 0.3);  // false !

Liukuluvun tarkkuus riittää noin 15–17 merkitsevään numeroon. Älä koskaan vertaa liukulukuja ===-operaattorilla vaan tutki erotusta:

Math.abs(0.1 + 0.2 - 0.3) < 1e-9;   // true

Tulokset on pyöristettävä sopivaan tarkkuuteen ennen näyttämistä, katso luku Pyöristäminen ja numeroiden muotoilu. Rahalaskennassa kannattaa laskea kokonaislukuina (sentteinä) tai käyttää BigIntiä.

2. Kokonaislukujen tarkkuus loppuu. Suurin turvallinen kokonaisluku on Number.MAX_SAFE_INTEGER eli 9 007 199 254 740 991 (2⁵³ − 1).

console.log(Number.MAX_SAFE_INTEGER);      // 9007199254740991
console.log(9007199254740992 === 9007199254740993);  // true !?

// jos tarvitset isompia kokonaislukuja, käytä BigIntiä (pääte n)
const iso = 9007199254740993n;
console.log(iso + 1n);                     // 9007199254740994n

BigIntiä ja tavallista Numberia ei voi sekoittaa keskenään aritmetiikassa (1n + 1 on TypeError).

Erikoisarvot

console.log(NaN === NaN);            // false! NaN ei ole yhtä suuri kuin mikään, ei edes itsensä
console.log(Number.isNaN(NaN));      // true   <-- käytä tätä
console.log(Object.is(NaN, NaN));    // true   <-- tai tätä

Object.is vastaa muuten ===-operaattoria, mutta se erottaa myös nollan etumerkin: Object.is(0, -0) on false, kun taas 0 === -0 on true. Katso Equality comparisons and sameness.

Katso myös Number.isInteger() ja Number.isFinite().

Tyyppimuunnokset

Kun luet numeron esimerkiksi lomakekentästä, saat aina merkkijonon. Muunna se numeroksi eksplisiittisesti.

Number("99")        // 99
Number("99.123")    // 99.123
Number("99a")       // NaN   — koko merkkijonon on kelvattava
Number("9a9")       // NaN
Number("a99")       // NaN
Number("1,5")       // NaN   — HUOM! desimaalipilkku ei kelpaa, vain piste

parseFloat("99")      // 99
parseFloat("99.123")  // 99.123
parseFloat("99a")     // 99   — lukee niin pitkälle kuin pystyy
parseFloat("9a9")     // 9
parseFloat("a99")     // NaN

parseInt("567")       // 567
parseInt("567", 10)   // 567 — toinen argumentti on kantaluku
parseInt("101", 2)    // 5   — binääriluku
parseInt("0x1F")      // 31  — 0x-alkuinen tulkitaan heksaluvuksi
parseInt("12.9")      // 12  — katkaisee, ei pyöristä

Kumpi valita? Number() on yleensä oikea valinta, koska se hylkää roskan sisältävät syötteet. parseInt/parseFloat lukevat merkkijonon alusta niin pitkälle kuin ymmärtävät, mikä on kätevää esimerkiksi CSS-arvon "12px" käsittelyssä mutta vaarallista käyttäjän syötteen validoinnissa.

Kantaluvun antaminen parseInt-funktiolle on silti hyvä tapa. Huomaa kuitenkin, että etunolla ei enää tarkoita oktaalilukua: parseInt("0567") on ES5:stä lähtien 567 kaikissa selaimissa, ei 375. Vain 0x-alku on erikoistapaus.

Number():in kanssa on yksi sudenkuoppa, joka kannattaa muistaa validoinnissa:

Number("")        // 0   <-- tyhjä merkkijono on nolla!
Number("   ")     // 0
Number(null)      // 0
Number(undefined) // NaN
Number(true)      // 1

Eli pelkkä Number.isNaN(Number(syote)) ei riitä tarkistamaan, että käyttäjä todella kirjoitti jotain. Tarkista tyhjyys erikseen:

function lueLuku(syote) {
  const trimmattu = syote.trim();
  if (trimmattu === "") {
    return null;              // ei syötettä
  }
  const luku = Number(trimmattu);
  return Number.isNaN(luku) ? null : luku;
}

Jos haluat erottaa liukuluvut kokonaisluvuista, Number.isInteger() auttaa.

Implisiittiset muunnokset

Javascript muuntaa tyyppejä automaattisesti myös silloin, kun et pyydä. Tulos riippuu operaattorista, ja erityisen kaksinaamainen on +: se on sekä yhteenlasku että merkkijonojen liittäminen.

console.log("wat" + 1);   // "wat1"  — numero muunnetaan merkkijonoksi ja liitetään
console.log("wat" - 1);   // NaN     — merkkijonoa ei saa numeroksi, joten tulos on NaN

console.log("3" + 1);     // "31"    — todennäköisesti ei sitä mitä halusit
console.log("3" - 1);     // 2       — ja tässä toimittiinkin numeroina
console.log([] + {});     // "[object Object]"
console.log([] + []);     // ""

Käytännön seuraus: jos luet luvun lomakkeelta ja unohdat muuntaa sen, hinta + 1 tuottaa merkkijonon "991" eikä lukua 100. Muunna syötteet numeroiksi heti, ja käytä merkkijonojen yhdistelyyn template-merkkijonoja +-operaattorin sijaan — silloin ei jää epäselväksi, kumpaa tarkoitit.

Tyypityksen termit

Tyypityksestä puhuttaessa sekoitetaan usein kaksi eri asiaa, jotka ovat toisistaan riippumattomia:

KysymysVaihtoehdotJavascript
Kirjoitetaanko tyyppi lähdekoodiin?staattinen / dynaaminendynaaminen — tyyppi selviää arvosta ajon aikana
Muuntaako kieli tyyppejä itsestään?vahva / heikkoheikko — muunnoksia tehdään automaattisesti

Huomaa, että arvolla on javascriptissä aina selvä ja pysyvä tyyppi; muuttujalla ei ole tyyppiä lainkaan. Kun sanotaan "muuttujan tyyppi muuttuu", tarkoitetaan sitä, että muuttujaan on sijoitettu toisentyyppinen arvo.

TypeScript lisää ensimmäiseen kysymykseen staattisen vaihtoehdon: tyypit kirjoitetaan koodiin ja tarkistetaan käännösaikana. Se ei muuta toista kysymystä — ajonaikainen käyttäytyminen on edelleen javascriptin.

Operaattorit

Käytössä ovat normaalit aritmeettiset ja vertailuoperaattorit. Vrt. Java.

Totuusarvot

Nämä kahdeksan arvoa ovat falsy eli epätosia totuusarvokontekstissa: false, 0, -0, 0n, "" (tyhjä merkkijono), null, undefined ja NaN. Kaikki muu on tosi — myös tyhjä taulukko [], tyhjä objekti {} ja merkkijono "false".

if ([]) { console.log("tämä tulostuu!"); }        // tyhjä taulukko on tosi
if ("0") { console.log("tämäkin tulostuu!"); }    // merkkijono "0" on tosi

Tarkista taulukon tyhjyys aina lista.length === 0, älä if (!lista).

!! antaa arvon totuusarvon: !!"teksti" on true.

Vertailu

=== tarkistaa, ovatko arvot samat ja samaa tyyppiä. Käytä aina === ja !==. Tavallinen == yrittää muuntaa arvot samantyyppisiksi, mikä johtaa sääntöihin, joita kukaan ei muista ulkoa.

1 === "1"        // false
1 == "1"         // true
0 == ""          // true
0 == "0"         // true
"" == "0"        // false !  (== ei ole transitiivinen)
null == undefined // true
null === undefined // false

Ainoa yleisesti hyväksytty poikkeus on arvo == null, joka tarkoittaa “on null tai undefined”.

Muut operaattorit

Nullish-operaattorit

Kaksi ES2020:n operaattoria, jotka lyhentävät koodia huomattavasti:

Valinnainen ketjutus ?. keskeyttää ja palauttaa undefined, jos vasemmalla puolella on null tai undefined:

// ilman:
const katu = henkilo && henkilo.osoite && henkilo.osoite.katu;
// kanssa:
const katu = henkilo?.osoite?.katu;

// toimii myös metodikutsuille ja indeksoinnille
elementti?.focus();
lista?.[0];

Nullish coalescing ?? antaa oletusarvon vain, jos vasen puoli on null tai undefined — toisin kuin ||, joka korvaa myös nollan ja tyhjän merkkijonon:

const maara1 = syote || 10;    // jos syote on 0, tulos on 10 — luultavasti bugi
const maara2 = syote ?? 10;    // jos syote on 0, tulos on 0 — yleensä oikein

Merkkijonot

Javascriptin merkkijonot ovat muuttumattomia (immutable). Kaikki merkkijono-operaatiot palauttavat aina uuden merkkijonon eivätkä muuta alkuperäistä.

Template-merkkijonot

Käytä template-merkkijonoja (takahipsut `) aina, kun yhdistelet tekstiä ja muuttujia. Ne ovat luettavampia kuin +-operaattori ja mahdollistavat monirivisen tekstin:

const nimi = "Oona";
const ika = 21;

// näin:
const teksti = `${nimi} on ${ika} vuotta vanha, ensi vuonna ${ika + 1}.`;

// eikä näin:
const teksti2 = nimi + " on " + ika + " vuotta vanha, ensi vuonna " + (ika + 1) + ".";

// monirivinen onnistuu suoraan
const viesti = `Hei ${nimi}!

Tervetuloa kurssille.`;

Tavallinen + toimii toki edelleen:

const foo = "tämä" + "tuo";

Myös taulukosta saa helposti merkkijonon halutulla erotinmerkillä:

const sanat = ["teksti1", "teksti2", "teksti3"];
console.log(sanat.join("; "));    // "teksti1; teksti2; teksti3"

Ominaisuudet ja metodit

Huomaa, että length on ominaisuus (ei metodi) — siinä ei ole sulkeita.

const s = "Jyväskylä";

s.length          // 9        (ominaisuus, ei s.length())
s[0]              // "J"      indeksointi kuten taulukossa
s.at(-1)          // "ä"      negatiivinen indeksi lopusta
s.charAt(0)       // "J"      sama kuin s[0]

s.indexOf("väs")  // 1        -1 jos ei löydy
s.includes("skyl") // true
s.startsWith("Jyv") // true
s.endsWith("lä")  // true

s.slice(0, 3)     // "Jyv"    alkuindeksi, loppuindeksi (ei mukaan)
s.slice(-3)       // "ylä"    negatiivinen indeksi toimii
s.substring(0, 3) // "Jyv"    HUOM: toinen parametri on LOPPUINDEKSI, ei pituus

s.toUpperCase()   // "JYVÄSKYLÄ"
s.toLowerCase()   // "jyväskylä"
"  x  ".trim()    // "x"      poistaa alusta ja lopusta tyhjät merkit
"a,b,c".split(",")     // ["a", "b", "c"]
"a-b-a".replaceAll("a", "x")  // "x-b-x"
String(5).padStart(2, "0")    // "05"

Yleinen virhe: substring(start, length) — toinen parametri ei ole pituus vaan loppuindeksi. Pituuden ottava substr on vanhentunut, älä käytä sitä. Käytä mieluiten slice, joka toimii samoin kuin taulukoiden slice.

Merkkijonojen vertailu

Merkkijonoja voi verrata operaattoreilla <, >, <=, >=, mutta ne eivät vertaile aakkosjärjestystä vaan Unicode-koodiarvoja:

"Aiti" < "aiti"    // tosi  — isot kirjaimet ovat ennen pieniä
"Äiti" < "aiti"    // epätosi, skandit sotkevat tilanteen edelliseen verrattuna
"ö" > "z"          // true  — ääkköset ovat z:n jälkeen
"apina" < "banaani" // true — tämä sattuu menemään oikein

Suomalaiseen aakkosjärjestykseen tarvitaan localeCompare tai Intl.Collator, katso luku localeCompare() ja Intl.Collator.

Ehtolauseet

if…else

if (ehto) {
  // jos oli tosi
}
else if (toinenEhto) {
  // jos ensimmäinen ei ollut mutta tämä on
}
else {
  // jos kumpikaan ei ollut
}

Kirjoita aaltosulkeet aina, myös yhden rivin lohkoihin. Yksi puuttuva aaltosulje on klassinen bugilähde.

Ehtolauseke (?:)

Lyhyisiin valintoihin sopii ehtolauseke:

const teksti = maara === 1 ? "yksi kappale" : `${maara} kappaletta`;

Älä ketjuta näitä sisäkkäin monta kerrosta — silloin if-lauseet ovat luettavampia.

switch

switch (arvo) {
  case "punainen":
    kasitteleP();
    break;
  case "vihreä":
    kasitteleV();
    break;
  default:
    kasitteleMuu();
    break;
}

Muista break jokaisen tapauksen lopussa. Ilman sitä suoritus jatkuu seuraavaan case-haaraan (“fall through”), mikä on lähes aina vahinko. Jos käytät läpivalumista tarkoituksella, kommentoi se selvästi:

switch (paiva) {
  case "la":
  case "su":
    // useampi case peräkkäin ilman koodia on hyväksytty tapa
    return "viikonloppu";
  default:
    return "arkipäivä";
}

Huomaa, että switch vertailee ===-operaattorilla, joten case 1: ei osu merkkijonoon "1".

Päätöstaulut monimutkaisiin ehtoihin

Joskus lopputulos riippuu monesta ehdosta hyvin mutkikkaalla tavalla. Kolmella ehdolla sisäkkäisistä if-lauseista tulee jo vaikeasti luettavia, ja neljännen ehdon jälkeen niitä ei enää käytännössä pysty ylläpitämään ilman virheitä. Tällöin kannattaa käyttää päätöstaulua.

Otetaan esimerkiksi kolme ehtoa — sataa, sataa paljon, on lämmin — ja halutaan tietää, kannattaako lähteä pyöräilemään. Kirjoitetaan ensin totuustaulu kaikille yhdistelmille:

nsataapaljonlämminpyöräilemäänmuuta
0FFFT
1FFTT
2FTFTei mahdollinen
3FTTTei mahdollinen
4TFFF
5TFTT
6TTFF
7TTTF

Sarake n saadaan tulkitsemalla ehtojen joukko binääriluvuksi:

T F T  =  1 0 1  =  5

Ne rivit, joita ei voi esiintyä (ei voi sataa paljon jos ei sada lainkaan), on merkitty taulukkoon ei mahdollinen. Niiden arvolla ei ole väliä, mutta rivi on silti kirjoitettava, jotta indeksointi pysyy oikeana.

Nyt sama helposti ylläpidettävänä koodina. Huomaa, että taulukon kommentit ovat suoraan yllä olevasta totuustaulusta — tämä on olennaista, koska silloin koodia ja määrittelyä voi verrata rivi riviltä:

function pyorailemaan(sataa, paljon, lammin) {
  const ehdot = [
          // | n | sataa | paljon | lämmin | pyöräilemään | muuta          |
    true,  // | 0 |  F    |   F    |   F    |      T       |                |
    true,  // | 1 |  F    |   F    |   T    |      T       |                |
    true,  // | 2 |  F    |   T    |   F    |      T       | ei mahdollinen |
    true,  // | 3 |  F    |   T    |   T    |      T       | ei mahdollinen |
    false, // | 4 |  T    |   F    |   F    |      F       |                |
    true,  // | 5 |  T    |   F    |   T    |      T       |                |
    false, // | 6 |  T    |   T    |   F    |      F       |                |
    false  // | 7 |  T    |   T    |   T    |      F       |                |
  ];

  const avain = 4 * sataa + 2 * paljon + 1 * lammin;
  return ehdot[avain];
}

console.log(pyorailemaan(true, false, true));   // true

Totuusarvot muuntuvat kertolaskussa automaattisesti numeroiksi (true on 1, false on 0). Jos haluat tehdä muunnoksen näkyväksi, kirjoita 4 * Number(sataa).

Sama kikka toimii silloinkin, kun palautettavaa tietoa on enemmän kuin yksi totuusarvo. Palautetaan totuusarvon sijaan objekti, jossa kerrotaan myös vaatetus ja kengät:

function pyorailemaan(sataa, paljon, lammin) {
  const ehdot = [
                                          // | n | sataa | paljon | lämmin |
    { mene: true,  vaate: 2, kengat: 2 }, // | 0 |  F    |   F    |   F    |
    { mene: true,  vaate: 1, kengat: 1 }, // | 1 |  F    |   F    |   T    |
    { mene: true,  vaate: 2, kengat: 2 }, // | 2 |  F    |   T    |   F    |
    { mene: true,  vaate: 2, kengat: 2 }, // | 3 |  F    |   T    |   T    |
    { mene: false, vaate: 2, kengat: 2 }, // | 4 |  T    |   F    |   F    |
    { mene: true,  vaate: 2, kengat: 2 }, // | 5 |  T    |   F    |   T    |
    { mene: false, vaate: 2, kengat: 2 }, // | 6 |  T    |   T    |   F    |
    { mene: false, vaate: 2, kengat: 2 }  // | 7 |  T    |   T    |   T    |
  ];

  const avain = 4 * sataa + 2 * paljon + 1 * lammin;
  return ehdot[avain];
}

const { mene, vaate, kengat } = pyorailemaan(true, false, true);

Kokonaisluvut 1 ja 2 ovat tässä huono ratkaisu — kukaan ei muista kuukauden päästä, kumpi tarkoitti sadekenkiä. Oikeassa koodissa palauttaisit kuvaavat merkkijonot tai vakioita. Kokeile muuttaa esimerkki sellaiseksi.

Seuraava askel tästä on laittaa taulukkoon funktioita, jolloin päätöstaulu valitsee valmiin arvon sijaan suoritettavan toiminnon.

Silmukat

for- ja while-silmukat toimivat kuten Javassa. Käytä silmukkamuuttujassa let-sanaa (for (let i = 0; ...)), jolloin muuttuja on silmukan sisäinen.

Labelilla voi nimetä tietyn silmukan, jolloin break- tai continue-komennolla voi viitata siihen:

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..of käy läpi arvot, for..in käy läpi avaimet.

const taul = [3, 5, 7];
taul.foo = "kukkuu";        // taulukkoon voi lisätä ominaisuuksia; älä tee näin

for (const avain in taul) {
  // huomaa, että avain on merkkijono!
  console.log(avain);       // "0", "1", "2", "foo"
}

for (const luku of taul) {
  console.log(luku);        // 3, 5, 7
}

for..in käy läpi objektin avaimet eli taulukon tapauksessa alkioiden paikkanumerot ja itse määriteltyjen ominaisuuksien nimet. Saadut avaimet ovat merkkijonoja eivätkä numeroita, ja järjestystäkään ei ole taattu kaikissa tapauksissa. Lisäksi for..in käy läpi myös prototyypiltä perityt ominaisuudet.

Käytä taulukoihin aina for..of-silmukkaa. Älä käytä for..in-silmukkaa taulukoiden käsittelyyn.

Entä objektit? Niihinkään for..in ei ole nykyään paras vaihtoehto, vaan Object.keys, Object.values tai Object.entries yhdistettynä for..of-silmukkaan. Tulos sisältää vain objektin omat ominaisuudet, ja koodista näkee heti, kumpaa (avainta vai arvoa) käsitellään:

const saatilat = {
  "jämsä": "Aurinkoinen",
  "äänekoski": "Pilvipoutaa"
};

for (const [paikka, saa] of Object.entries(saatilat)) {
  console.log(`${paikka}: ${saa}`);
}

for (const paikka of Object.keys(saatilat)) { ... }
for (const saa of Object.values(saatilat)) { ... }

Jos indeksi tarvitaan taulukkoa läpikäydessä, käytä entries()-metodia:

for (const [i, arvo] of taul.entries()) {
  console.log(`${i}: ${arvo}`);
}

Lisätietoa:

Esimerkki: obj.html?js=1&console=1

Poikkeukset

try {
  // kokeillaan jotain
  if (!kelpaa(syote)) {
    throw new Error(`Kelvoton syöte: ${syote}`);
  }
}
catch (virhe) {
  console.error(virhe.message);
}
finally {
  // tämä suoritetaan aina, tuli poikkeus tai ei
}

Heitä aina Error-objekti (tai siitä peritty), älä merkkijonoa. Vain Error-objektissa on message, name ja stack, joista kaksi viimeistä ovat korvaamattomia debuggauksessa.

throw new Error("selkokielinen kuvaus");        // näin
throw "no ny sekos";                            // älä näin

Voit määritellä oman virhetyypin perimällä:

class SyoteVirhe extends Error {
  constructor(viesti, kentta) {
    super(viesti);
    this.name = "SyoteVirhe";
    this.kentta = kentta;
  }
}

Jos et tarvitse virheobjektia, catch-lohkon parametrin voi jättää poiskin: catch { ... }.

Katso Error types.

Funktiot

Funktion voi esitellä usealla tavalla:

// 1. funktiomäärittely (function declaration)
function summaa(x, y) {
  return x + y;
}

// 2. funktiolauseke (function expression)
const summaa2 = function (x, y) {
  return x + y;
};

// 3. nuolifunktio (arrow function) — lyhin muoto
const summaa3 = (x, y) => x + y;

Nuolifunktiot ovat kätevimpiä lyhyissä takaisinkutsuissa (map, filter, sort, tapahtumankäsittelijät). Jos rungossa on vain yksi lauseke, return ja aaltosulkeet voi jättää pois:

const nelio = x => x * x;
const pituudet = sanat.map(sana => sana.length);

// useampi rivi vaatii aaltosulkeet ja return:
const kuvaile = sana => {
  const p = sana.length;
  return `${sana} (${p} merkkiä)`;
};

// jos palautat objektin, kääri se sulkeisiin:
const teeObjekti = nimi => ({ nimi: nimi });

Nuolifunktiot eroavat tavallisista siinä, että niillä ei ole omaa this- eikä arguments-sidontaa vaan ne perivät ne ympäröivästä koodista. Tämä on yleensä juuri se, mitä halutaan.

Parametrit

// oletusarvot
function f(x = 0, y = 1) {
  return x + y;
}
f();        // 1
f(5);       // 6

// rest-parametri kerää loput argumentit taulukoksi
function summaaKaikki(...luvut) {
  return luvut.reduce((a, b) => a + b, 0);
}
summaaKaikki(1, 2, 3, 4);    // 10

// spread hajottaa taulukon argumenteiksi
const luvut = [3, 1, 4];
Math.max(...luvut);          // 4

Rest-parametri ...luvut korvaa vanhan arguments-objektin, joka ei ole oikea taulukko eikä toimi nuolifunktioissa. Käytä rest-parametria.

Funktiota voi kutsua ylimääräisillä argumenteilla ilman virhettä; puuttuvat parametrit saavat arvon undefined.

Jos funktio ei palauta return-lauseella arvoa, palautusarvo on undefined.

Sulkeumat (closure)

Sisäkkäinen funktio näkee ulomman funktion muuttujat myös sen jälkeen, kun ulompi funktio on jo päättynyt. Tätä kutsutaan sulkeumaksi:

function teeLaskuri() {
  let arvo = 0;                 // "yksityinen" muuttuja
  return function () {
    arvo = arvo + 1;
    return arvo;
  };
}

const laskuri = teeLaskuri();
laskuri();   // 1
laskuri();   // 2

Argumenttien välitys

Javascript välittää argumentit aina arvona. Objektien ja taulukoiden kohdalla se arvo on kuitenkin viite objektiin, joten lopputulos näyttää ensin sekavalta:

function muuta(lista, luku) {
  lista.push(4);        // näkyy kutsujalle — muutetaan samaa objektia
  lista = [9, 9, 9];    // EI näy kutsujalle — vaihdetaan vain paikallinen viite
  luku = 100;           // EI näy kutsujalle
}

const omaLista = [1, 2, 3];
let omaLuku = 1;
muuta(omaLista, omaLuku);
console.log(omaLista);   // [1, 2, 3, 4]
console.log(omaLuku);    // 1

Sääntö on siis: objektin sisällön muuttaminen näkyy kutsujalle, parametrin uudelleensijoitus ei.

Esimerkki: func?js=1&console=1

Funktion kuormitusta ei ole

Toisin kuin Javassa tai C#:ssa, javascriptissä ei voi määritellä kahta samannimistä funktiota eri parametreilla. Jälkimmäinen määrittely yksinkertaisesti korvaa edellisen. Jos funktion pitää toimia eri määrällä argumentteja, se on testattava itse:

function lisaa(a, maara) {
  if (maara !== undefined) {
    return a + maara;
  }
  return a + 1;
}

console.log(lisaa(2));      // 3
console.log(lisaa(2, 0));   // 2
console.log(lisaa(3, 4));   // 7

Sama kirjoitetaan yleensä lyhyemmin oletusarvolla: function lisaa(a, maara = 1).

Älä käytä tähän ||-operaattoria. Seuraava näyttää toimivan, mutta on rikki:

return a + (maara || 1);   // VÄÄRIN

Kun maara on 0, se on epätosi (falsy), joten tulokseksi tulee 1 eikä nolla. Tämä on täsmälleen sama sudenkuoppa kuin luvussa Nullish-operaattorit: käytä ??-operaattoria tai oletusarvoa, jotka reagoivat vain arvoihin null ja undefined.

Funktiokin on olio

Javascriptissä funktiot ovat olioita. Niillä voi siis olla ominaisuuksia ja metodeja aivan kuten muillakin objekteilla — funktioksi ne tekee se, että niitä voi kutsua ()-operaattorilla.

function laske(x) {
  laske.kutsuja = laske.kutsuja + 1;
  return x * 2;
}
laske.kutsuja = 0;          // funktio-oliolle asetettu ominaisuus

laske(21);
laske(5);
console.log(laske.kutsuja); // 2
console.log(laske.name);    // "laske"   valmiina oleva ominaisuus
console.log(laske.length);  // 1         parametrien lukumäärä

Koska funktio on arvo, sen voi myös sijoittaa muuttujaan, antaa parametrina toiselle funktiolle ja palauttaa funktiosta. Juuri tähän perustuvat sort-vertailufunktiot ja map-tyyliset metodit.

Funktio-olioilla on lisäksi metodit call ja apply, joilla funktion voi kutsua määrätyllä this-viitteellä. Näitä näkee vanhassa koodissa; nykyään sama hoituu yleensä nuolifunktiolla tai luokan metodilla.

laske.call(null, 21);       // argumentit erikseen
laske.apply(null, [21]);    // argumentit taulukkona

Tietorakenteet

Luentovideo: Javascript-tietorakenteet. Jos video ei näy, katso se suoraan YouTubesta.

Taulukot

Taulukko luodaan seuraavasti:

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

tai suoraan:

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

Huomaa:

const paikat = ["Jämsä", "Äänekoski"];
paikat[100] = "Helsinki";
console.log(paikat.length);   // 101 eikä 3!

Väliin jää “reikiä” (sparse array), joiden kohdalla arvo on undefined. Vältä tätä — lisää aina loppuun.

Lisääminen ja poistaminen

paikat.push("Jyväskylä");     // lisää loppuun (suositeltu tapa)
paikat.pop();                 // poistaa ja palauttaa viimeisen
paikat.unshift("Oulu");       // lisää alkuun
paikat.shift();               // poistaa ja palauttaa ensimmäisen
paikat.splice(1, 2);          // poistaa 2 alkiota indeksistä 1 alkaen
paikat.splice(1, 0, "Turku"); // lisää keskelle poistamatta mitään

Hakeminen

const luvut = [3, 5, 7, 5];

luvut.includes(5)                 // true
luvut.indexOf(5)                  // 1   (-1 jos ei löydy)
luvut.lastIndexOf(5)              // 3
luvut.at(-1)                      // 5   viimeinen alkio
luvut.find(x => x > 4)            // 5   ensimmäinen ehdon täyttävä ARVO
luvut.findIndex(x => x > 4)       // 1   ensimmäisen ehdon täyttävän INDEKSI

Yhdistäminen merkkijonoksi

["a", "b", "c"].join(", ")   // "a, b, c"

map, forEach, filter, some, every ja reduce

Nämä taulukon metodit korvaavat suuren osan silmukoista lyhyemmällä ja luettavammalla koodilla. Kukin niistä ottaa parametrikseen funktion, jota kutsutaan jokaiselle alkiolle.

Esimerkkiaineisto:

const opiskelijat = [
  { nimi: "Oona",  pisteet: 42, tutkinto: "LuK" },
  { nimi: "Olli",  pisteet: 17, tutkinto: "LuK" },
  { nimi: "Tommi", pisteet: 31, tutkinto: "FM"  }
];

map muuntaa jokaisen alkion ja palauttaa uuden, yhtä pitkän taulukon. Alkuperäinen ei muutu.

const nimet = opiskelijat.map(o => o.nimi);
// ["Oona", "Olli", "Tommi"]

const korotetut = opiskelijat.map(o => ({ ...o, pisteet: o.pisteet + 5 }));

filter valitsee alkiot, joilla funktio palauttaa true. Palauttaa uuden, mahdollisesti lyhyemmän taulukon.

const lapaisseet = opiskelijat.filter(o => o.pisteet >= 25);
// Oona ja Tommi

forEach suorittaa funktion jokaiselle alkiolle eikä palauta mitään. Käytä sitä vain sivuvaikutuksiin (esim. tulostukseen tai DOM:in päivitykseen). Jos rakennat uutta taulukkoa, map on oikea valinta.

opiskelijat.forEach(o => console.log(`${o.nimi}: ${o.pisteet}`));

some ja every palauttavat totuusarvon: onko jollakin / onko kaikilla ehto tosi.

opiskelijat.some(o => o.pisteet > 40);    // true
opiskelijat.every(o => o.pisteet > 40);   // false

reduce kokoaa taulukon yhdeksi arvoksi. Ensimmäinen parametri on funktio (kertyma, alkio) => uusiKertyma ja toinen parametri on kertymän alkuarvo — anna se aina, muuten tyhjä taulukko heittää poikkeuksen.

const yhteensa = opiskelijat.reduce((summa, o) => summa + o.pisteet, 0);
// 90

// reducella voi koota myös objektin: ryhmittely tutkinnon mukaan
const ryhmat = opiskelijat.reduce((kertyma, o) => {
  if (!kertyma[o.tutkinto]) {
    kertyma[o.tutkinto] = [];
  }
  kertyma[o.tutkinto].push(o.nimi);
  return kertyma;
}, {});
// { LuK: ["Oona", "Olli"], FM: ["Tommi"] }

Jos reduce alkaa näyttää vaikeaselkoiselta, tavallinen for..of-silmukka on täysin hyväksyttävä vaihtoehto. Ryhmittelyyn on nykyään myös valmis Object.groupBy.

Metodit voi ketjuttaa, koska jokainen palauttaa uuden taulukon:

const tulos = opiskelijat
  .filter(o => o.pisteet >= 25)
  .map(o => o.nimi)
  .join(", ");
// "Oona, Tommi"

Esimerkit: map2.html?js=1&console=1 sekä map.html ja map.js

Taulukon kopioiminen

Taulukot ja objektit ovat viitteitä. Taulukkoa ei voi kopioida pelkällä sijoitusoperaatiolla.

Pinnallinen (shallow) kopio kopioi taulukon, mutta sen alkioina olevat objektit jäävät jaetuiksi:

const mallitaulukko = [
  { "foo": "bar" },
  { "foo": "foo" },
  { "foo": "foobar" },
  { "foo": "barfoo" }
];

// kaikki seuraavat tekevät shallow-kopion
let kopio = [...mallitaulukko];              // levitys (spread), yleisin tapa
kopio = Array.from(mallitaulukko);
kopio = mallitaulukko.slice();

kopio = [];
for (const malli of mallitaulukko) {
  kopio.push(malli);
}

// seuraava on VÄÄRIN eikä tee kopiota taulukosta:
// kopio = mallitaulukko;   // kopioi vain viitteen samaan taulukkoon

Kopioiminen kannattaa, koska osa metodeista muuttaa taulukkoa paikallaan. Esimerkiksi sort järjestää kohteensa eikä palauta uutta taulukkoa:

kopio.sort((a, b) => a.foo.localeCompare(b.foo, "fi", { sensitivity: "base" }));
console.log("malli", mallitaulukko);
// [{"foo":"bar"},{"foo":"foo"},{"foo":"foobar"},{"foo":"barfoo"}]  — alkuperäinen järjestys
console.log("kopio", kopio);
// [{"foo":"bar"},{"foo":"barfoo"},{"foo":"foo"},{"foo":"foobar"}]  — järjestetty

Koska kopio on pinnallinen, siinä on kuitenkin viittaukset samoihin objekteihin kuin alkuperäisessä:

kopio[1]["foo"] = "testi";
console.log("malli", mallitaulukko);
// [{"foo":"bar"},{"foo":"foo"},{"foo":"foobar"},{"foo":"testi"}]  <-- muuttui!
console.log("kopio", kopio);
// [{"foo":"bar"},{"foo":"testi"},{"foo":"foo"},{"foo":"foobar"}]

Syväkopio (deep copy) kopioi myös sisällä olevat objektit. Nykyään tähän on valmis funktio structuredClone(), joka osaa myös ristiin menevät viitteet sekä Date-, Map- ja Set-objektit:

const syvakopio = structuredClone(mallitaulukko);
syvakopio[3]["foo"] = "testi2";
console.log("malli", mallitaulukko);      // ei muutu
console.log("kopio", syvakopio);

structuredClone ei osaa kopioida funktioita, DOM-elementtejä eikä luokkien tyyppitietoa (luokan ilmentymästä tulee tavallinen objekti). Jos rakenteessa on tällaista, kirjoita kopiointi itse:

const kopio2 = mallitaulukko.map(obj => ({ "foo": obj.foo }));

Vanha JSON-kikka toimii edelleen, mutta on hitaampi ja kaatuu ristiin meneviin viitteisiin. Lisäksi se hukkaa undefined-arvot ja muuttaa päivämäärät merkkijonoiksi:

const kopio3 = JSON.parse(JSON.stringify(mallitaulukko));

Esimerkki: kopiointi.html?js=1&console=1

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 tyhjäksi paikaksi. Taulukon koko tai indeksointi ei muutu. delete on tarkoitettu objektien ominaisuuksien poistamiseen eikä taulukon alkioiden poistamiseen.

const a = ["a", "b", "c"];
delete a[1];
console.log(a);          // [ "a", <1 empty slot>, "c" ]
console.log(a.length);   // 3  — pituus ei muuttunut!

const b = ["a", "b", "c"];
b.splice(1, 1);
console.log(b);          // ["a", "c"]
console.log(b.length);   // 2  — näin pitää tehdä

Katso myös: Deleting array elements in JavaScript — delete vs splice

Set

Set on joukko: kokoelma, jossa jokainen arvo esiintyy korkeintaan kerran. Se ei ole taulukko — alkioilla ei ole indeksejä.

const joukko = new Set();
joukko.add("Jämsä");
joukko.add("Äänekoski");
joukko.add("Jämsä");            // ei tee mitään, arvo on jo joukossa

joukko.size;                    // 2   (ei length!)
joukko.has("Jämsä");            // true
joukko.delete("Jämsä");         // true, jos oli olemassa

for (const paikka of joukko) {
  console.log(paikka);
}

Setin tavallisin käyttö on kaksoiskappaleiden poisto taulukosta:

const luvut = [1, 2, 2, 3, 3, 3];
const uniikit = [...new Set(luvut)];    // [1, 2, 3]

has on isolla aineistolla huomattavasti nopeampi kuin taulukon includes, joten jos tarkistat samasta kokoelmasta jäsenyyttä silmukassa, kannattaa rakentaa Set.

Setille on myös joukko-operaatiot (ES2025): union, intersection, difference, isSubsetOf.

const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
a.intersection(b);   // Set { 2, 3 }
a.union(b);          // Set { 1, 2, 3, 4 }

Huomaa, että Set vertaa arvoja kuten ===. Kaksi erillistä objektia ovat siis eri alkioita, vaikka niiden sisältö olisi identtinen.

Map

Map on tietorakenne avain–arvo-pareille. Vrt. Pythonin dict tai Javan HashMap.

const lampotilat = new Map();
lampotilat.set("jkl", -6);
lampotilat.set("oulu", -14);

lampotilat.get("jkl");        // -6
lampotilat.get("turku");      // undefined
lampotilat.has("oulu");       // true
lampotilat.delete("oulu");    // true
lampotilat.size;              // 1

// alustus suoraan pareista
const saatilat = new Map([
  ["jämsä", "Aurinkoinen"],
  ["äänekoski", "Pilvipoutaa"]
]);

// läpikäynti: for..of antaa [avain, arvo] -parit
for (const [paikka, saa] of saatilat) {
  console.log(`${paikka}: ${saa}`);
}

Map vai objekti?

Molemmilla voi tallentaa avain–arvo-pareja, mutta ne eivät ole sama asia:

Map objekti {}
Avaimen tyyppi mikä tahansa arvo, myös objekti tai numero vain merkkijono tai Symbol
Koko map.size Object.keys(obj).length
Järjestys lisäysjärjestys taattu numeeriset avaimet ensin, sitten lisäysjärjestys
Perityt avaimet ei ole prototyypin ominaisuudet voivat häiritä
JSON ei tue suoraan tukee suoraan
Läpikäynti for..of suoraan Object.entries()

Nyrkkisääntö: jos avaimet ovat dataa (esim. käyttäjän syöttämiä tunnisteita, joita voi olla mielivaltainen määrä), käytä Map. Jos avaimet ovat kiinteä joukko kenttien nimiä (nimi, osoite, ika), käytä objektia.

Objektin kanssa on myös hyvä muistaa, että avaimet muunnetaan aina merkkijonoiksi:

const obj = {};
obj[1] = "a";
obj["1"] = "b";
console.log(obj);        // { "1": "b" }  — sama avain!

const map = new Map();
map.set(1, "a");
map.set("1", "b");
console.log(map.size);   // 2  — eri avaimet

Mapin saa muunnettua objektiksi ja takaisin:

const obj2 = Object.fromEntries(saatilat);
const map2 = new Map(Object.entries(obj2));

Objektit

Objekti on kokoelma nimi–arvo-pareja. Luo objektit objektiliteraalilla {}, älä new Object():lla.

const saatilat = {
  "jämsä": "Aurinkoinen",
  "äänekoski": "Pilvipoutaa"
};

const henkilo = {
  etunimi: "Oona",              // lainausmerkit voi jättää pois yksinkertaisista nimistä
  sukunimi: "Opiskelija",
  ika: 21,
  osoite: {                     // objekteja voi upottaa
    katu: "Ylistönmäentie 33",
    postinumero: "40500"
  },
  kokoNimi() {                  // metodi
    return `${this.etunimi} ${this.sukunimi}`;
  }
};

Ominaisuuksiin viitataan kahdella tavalla, jotka tarkoittavat samaa: henkilo.etunimi ja henkilo["etunimi"]. Hakasulkumuotoa tarvitaan, kun avain on muuttujassa tai kun se ei ole kelvollinen tunniste:

const kentta = "etunimi";
console.log(henkilo[kentta]);       // "Oona" — piste ei toimisi tässä
console.log(saatilat["jämsä"]);

Ominaisuuksia voi lisätä ja poistaa vapaasti:

henkilo.puhelin = "040 1234567";    // lisääminen
delete henkilo.puhelin;             // poistaminen

console.log(henkilo.sposti);              // undefined — ei virhe!
console.log("sposti" in henkilo);         // false
console.log(henkilo.sposti === undefined); // true

Huomaa, että olemattoman ominaisuuden lukeminen palauttaa undefined eikä aiheuta virhettä. Tämä on yleisin syy siihen, että kirjoitusvirhe avaimen nimessä jää huomaamatta.

Purku (destructuring)

Objektista ja taulukosta saa arvot muuttujiin kätevästi purkamalla:

const { etunimi, sukunimi } = henkilo;
console.log(etunimi);                       // "Oona"

// uudelleennimeäminen ja oletusarvo
const { ika: vuodet, sposti = "ei tiedossa" } = henkilo;

// sisäkkäinen
const { osoite: { postinumero } } = henkilo;

// taulukot puretaan järjestyksessä
const [eka, toka] = ["a", "b", "c"];

// toimii myös funktion parametrina
function tulosta({ etunimi, sukunimi }) {
  console.log(`${sukunimi} ${etunimi}`);
}

Levitys (spread) ja kopiointi

// pinnallinen kopio
const kopio = { ...henkilo };
const kopio2 = Object.assign({}, henkilo);

// kopio, jossa yksi kenttä muutettu
const vanhempi = { ...henkilo, ika: 22 };

// kahden objektin yhdistäminen (jälkimmäinen voittaa)
const yhdistetty = { ...oletukset, ...omatAsetukset };

Nämä ovat pinnallisia kopioita: kopio.osoite on sama objekti kuin henkilo.osoite. Syväkopioon käytä structuredClone.

Objektin läpikäynti

Object.keys(saatilat);      // ["jämsä", "äänekoski"]
Object.values(saatilat);    // ["Aurinkoinen", "Pilvipoutaa"]
Object.entries(saatilat);   // [["jämsä", "Aurinkoinen"], ["äänekoski", "Pilvipoutaa"]]

for (const [paikka, saa] of Object.entries(saatilat)) {
  console.log(`${paikka}: ${saa}`);
}

Sisäkkäiset tietorakenteet

Verkosta saatava JSON-data on käytännössä aina sisäkkäistä: taulukossa on objekteja, joiden kentissä on lisää taulukoita ja objekteja. Poraudu rakenteeseen yksi taso kerrallaan. Taulukosta valitaan alkio numeroindeksillä ja objektista kenttä nimellä, ja näitä vuorotellaan:

const opiskelijat = [
  {
    "nimi": "Maija Meikäläinen",
    "syntymävuosi": 1999,
    "pääaine": "TIE",
    "kurssit": ["TIEA2120", "ITKP101", "ITKP104"]
  },
  {
    "nimi": "Matti Meikäläinen",
    "syntymävuosi": 1999,
    "pääaine": "TIE"
  }
];

opiskelijat[0]                    // ensimmäinen objekti
opiskelijat[0].nimi               // "Maija Meikäläinen"
opiskelijat[0]["nimi"]            // sama asia
opiskelijat[0].kurssit            // ["TIEA2120", "ITKP101", "ITKP104"]
opiskelijat[0].kurssit[1]         // "ITKP101"
opiskelijat[1].kurssit            // undefined — kenttää ei ole
opiskelijat[1].kurssit[0]         // TypeError!

Viimeinen rivi on tavallisin virhe tällaisen datan kanssa: undefined-arvosta ei voi hakea alkiota. Tähän on kolme ratkaisua, joista viimeinen on lyhin:

if (opiskelijat[1].kurssit) { ... }              // testaa ensin
const kurssit = opiskelijat[1].kurssit ?? [];    // oletusarvo
opiskelijat[1].kurssit?.[0];                     // valinnainen ketjutus, palauttaa undefined

Jos avain on muuttujassa, käytä hakasulkeita. Tämä on tyypillistä esimerkiksi silloin, kun käyttäjä valitsee lajitteluperusteen:

const kentta = "pääaine";
for (const opis of opiskelijat) {
  console.log(opis[kentta]);      // opis.kentta hakisi kenttää nimeltä "kentta"
}

Koko rakenteen läpikäynti onnistuu sisäkkäisillä silmukoilla, mutta koodi pysyy luettavampana, kun jokainen taso käsitellään omassa funktiossaan:

function kuvaaOpiskelija(opis) {
  const kurssit = opis.kurssit ?? [];
  return `${opis.nimi} (${opis.pääaine}): ${kurssit.length} kurssia`;
}

for (const opis of opiskelijat) {
  console.log(kuvaaOpiskelija(opis));
}

// kaikki kurssit yhtenä listana ilman kaksoiskappaleita
const kaikkiKurssit = [...new Set(
  opiskelijat.flatMap(opis => opis.kurssit ?? [])
)];

flatMap toimii kuten map, mutta purkaa lopuksi yhden tason sisäkkäisiä taulukoita. Se on kätevä juuri tällaisissa tilanteissa.

Tuntemattoman rakenteen tutkimiseen paras työkalu on selaimen konsoli: console.log(data) ja rakenteen avaaminen kolmioista, tai console.table(data) kun alkiot ovat samanmuotoisia objekteja. Myös JSON editor online auttaa hahmottamaan isoa aineistoa.

Luokat

JavaScript on prototyyppipohjainen kieli: siinä ei ole luokkia samassa mielessä kuin Java/C++/C#-kielissä vaan pelkästään olioita. class-syntaksi on prototyyppien päälle rakennettu helpotus, ja sitä kannattaa käyttää.

class Opiskelija {
  #opiskelijanumero;             // yksityinen kenttä (risuaita on osa nimeä)

  constructor(etunimi, sukunimi, opiskelijanumero) {
    this.etunimi = etunimi;
    this.sukunimi = sukunimi;
    this.#opiskelijanumero = opiskelijanumero;
  }

  // metodi
  kokoNimi() {
    return `${this.sukunimi} ${this.etunimi}`;
  }

  // getter: käytetään ilman sulkeita, opiskelija.numero
  get numero() {
    return this.#opiskelijanumero;
  }

  // oma toString-metodi
  toString() {
    return this.kokoNimi();
  }
}

const oona = new Opiskelija("Oona", "Opiskelija", "123456");
console.log(oona.kokoNimi());     // "Opiskelija Oona"
console.log(oona.numero);         // "123456"
console.log(`${oona}`);           // "Opiskelija Oona"  — toString kutsutaan
console.log(oona.#opiskelijanumero); // SyntaxError: yksityiseen ei pääse ulkopuolelta

Periytyminen:

class Polygon {
  constructor(korkeus, leveys) {
    this.nimi = "Polygon";
    this.korkeus = korkeus;
    this.leveys = leveys;
  }
  pintaAla() {
    return this.korkeus * this.leveys;
  }
}

class Nelio extends Polygon {
  constructor(sivu) {
    // super kutsuu Polygonin konstruktoria.
    // Suoritettava ennen kuin voi käyttää this-viitettä.
    super(sivu, sivu);
    this.nimi = "Nelio";
  }
}

const nelio = new Nelio(20);
console.log(nelio.pintaAla());     // 400
console.log(nelio instanceof Polygon);   // true

Vanhemmissa esimerkeissä näkee prototyyppisyntaksia. Se toimii edelleen ja on hyvä ymmärtää, mutta uutta koodia ei kannata kirjoittaa näin:

function Koordinaatti(x, y) {
  this.x = x;
  this.y = y;
}
Koordinaatti.prototype.normi = function () {
  return Math.sqrt(this.x ** 2 + this.y ** 2);
};
const piste = new Koordinaatti(1, 2);
piste.normi();

Katso: Details of the object model ja yksityiset ominaisuudet.

Esimerkki: struct.html?js=1&console=1

JSON

JSON (JavaScript Object Notation) on standardoitu tekstimuotoinen tiedonsiirtoformaatti. Se on syntaksiltaan lähes sama asia kuin Javascriptin objekti- ja taulukkoliteraalit, ja WWW-sovelluksissa se on ylivoimaisesti yleisin tapa siirtää dataa palvelimen ja selaimen välillä. Lähes kaikki ohjelmointikielet osaavat lukea ja kirjoittaa JSONia.

{
  "kurssi": "TIEA2120",
  "opintopisteet": 5,
  "pakollinen": false,
  "luennoitsijat": ["Tommi Lahtonen"],
  "kuvaus": null
}

JSON ja JavaScript eivät ole sama asia

JSON on osajoukko Javascriptin literaalisyntaksista, ja erot kaatavat koodin herkästi:

JSON.stringify ja JSON.parse

const olio = { nimi: "Oona", pisteet: 42 };

const teksti = JSON.stringify(olio);
// '{"nimi":"Oona","pisteet":42}'

// kolmas parametri on sisennys — lukukelpoista tulostetta varten
console.log(JSON.stringify(olio, null, 2));

const takaisin = JSON.parse(teksti);
console.log(takaisin.nimi);      // "Oona"

JSON.parse heittää poikkeuksen, jos syöte ei ole kelvollista JSONia, joten kääri se try-lohkoon aina kun data tulee ulkopuolelta:

try {
  const data = JSON.parse(vastaus);
}
catch (virhe) {
  console.error("Virheellinen JSON:", virhe.message);
}

Muunnoksessa katoaa tietoa, ja tämä yllättää säännöllisesti:

JSON.stringify({ a: undefined, b: () => 1, c: NaN, d: new Date(), e: new Map() });
// '{"c":null,"d":"2026-09-07T10:00:00.000Z","e":{}}'
// a ja b katosivat, NaN muuttui nulliksi, Date merkkijonoksi, Map tyhjäksi objektiksi

Päivämäärät on siis luettava takaisin itse. Jos objektilla on toJSON-metodi, JSON.stringify käyttää sitä.

Tutki ensimmäisen viikkotehtävän tietorakenteita selaimen konsolissa: avaa kuluvan vuoden pohja.xhtml viikkotehtäväsivulta ja sen jälkeen konsoli.

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Ä KIRJOITA OMAA JÄRJESTÄMISALGORITMIA.

Käytä ohjelmointikielen kirjastojen tarjoamia. Joku muu on jo toteuttanut sinua paremmin tehokkaan järjestämisen. Luottakaa 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?

sort muuttaa taulukkoa

Ennen kuin mennään vertailufunktioihin, yksi tärkeä yksityiskohta: sort() järjestää sen taulukon, jolle sitä kutsutaan, ja palauttaa viitteen samaan taulukkoon. Se ei tee kopiota.

const alkuperainen = [3, 1, 2];
const jarjestetty = alkuperainen.sort();
console.log(alkuperainen);    // [1, 2, 3]  <-- muuttui myös!

Jos haluat säilyttää alkuperäisen järjestyksen, käytä toSorted()-metodia (ES2023), joka palauttaa uuden taulukon:

const jarjestetty = alkuperainen.toSorted();
console.log(alkuperainen);    // [3, 1, 2]  — ennallaan

Samaan tapaan toimivat toReversed(), toSpliced() ja with(). Vanha tapa on kopioida ensin: [...alkuperainen].sort().

sort on ollut ES2019:stä lähtien vakaa (stable), eli yhtä suuriksi katsotut alkiot säilyttävät keskinäisen järjestyksensä. Tämän ansiosta voi järjestää kahdesti peräkkäin: ensin etunimen, sitten sukunimen mukaan.

Vertailufunktio

Aloitetaan alusta. Javascriptissa järjestäminen toimii hienosti merkkijonoilla:

const taulukko = ["foo", "bar", "foobar"];
taulukko.sort();
// ["bar", "foo", "foobar"]

Entäpä seuraava:

const luvut = [10, 5, 40];
luvut.sort();
// [10, 40, 5]

Väärin meni. Javascriptin sort() muuntaa oletuksena kaiken merkkijonoiksi. Tässä tapauksessa myös numerot muuntuvat merkkijonoiksi ja järjestyvät väärin.

Miten tämä korjataan? Järjestämisessä olennainen kysymys on, miten kaksi alkiota laitetaan suuruusjärjestykseen. Riippumatta siitä, minkälainen järjestettävä objekti on, järjestäminen onnistuu, jos jotenkin voimme määrätä kahden alkion keskinäisen järjestyksen.

sort()-funktiolle voidaan antaa oma vertailufunktio. Sen sopimus on:

Palautusarvo Merkitys
negatiivinen a tulee ennen b:tä
0 järjestys ei muutu (yhtä suuret)
positiivinen b tulee ennen a:ta

Jos on huolimaton tämän funktion kanssa, ampuu helposti itseään jalkaan. Väärin kirjoitettu funktio voi tuurilla toimia yhdessä selaimessa mutta ei toisessa. Vertailufunktion on oltava johdonmukainen: jos a < b ja b < c, on oltava a < c.

Numeroiden vertailu voidaan kirjoittaa näin yksinkertaisesti:

function vertaaLuvut(a, b) {
  return a - b;
}

luvut.sort(vertaaLuvut);
// [5, 10, 40]

Nyt toimii! Huomatkaa, että sort()-funktiolle annetaan parametrina tuon oman vertailufunktion osoitin eli javascriptissa pelkkä funktion nimi.

Sama nuolifunktiona: luvut.sort((a, b) => a - b);

Objektien järjestäminen

let taulukko = [
  { "etunimi": "Oona",  "sukunimi": "Opiskelija" },
  { "etunimi": "Olli",  "sukunimi": "Opiskelija" },
  { "etunimi": "Tommi", "sukunimi": "Lahtonen" }
];

Javascript ei osaa muuntaa objekteja automaattisesti järkeviksi merkkijonoiksi, joten tarvitaan oma vertailufunktio.

Yleinen aina toimiva vertailufunktion muoto olisi seuraava:

function vertaa(a, b) {
  if (a < b) {
    return -1;
  }
  if (a > b) {
    return 1;
  }
  return 0;    // yhtä suuret
}

Tämä ei tietenkään suoraan toimi, vaan nyt pitää muuttaa muuttujien a ja b käsittely vastaamaan omia objektejamme eli vertailla sukunimiä:

function vertaa(a, b) {
  if (a.sukunimi < b.sukunimi) {
    return -1;
  }
  if (a.sukunimi > b.sukunimi) {
    return 1;
  }
  return 0;
}

taulukko.sort(vertaa);
// 0: { etunimi: "Tommi", sukunimi: "Lahtonen" }
// 1: { etunimi: "Oona",  sukunimi: "Opiskelija" }
// 2: { etunimi: "Olli",  sukunimi: "Opiskelija" }

Järjestäminen toimii, mutta ei aivan täysin: pitäisi vertailla myös etunimiä, koska sukunimi voi olla sama. Parannellaan funktiota. Huomaa, että etunimivertailun on mentävä samaan suuntaan kuin sukunimivertailun — muuten etunimet järjestyvät päinvastoin:

function vertaa(a, b) {
  if (a.sukunimi < b.sukunimi) {
    return -1;
  }
  if (a.sukunimi > b.sukunimi) {
    return 1;
  }
  // sukunimet olivat samat, vertaillaan etunimiä
  if (a.etunimi < b.etunimi) {
    return -1;
  }
  if (a.etunimi > b.etunimi) {
    return 1;
  }
  return 0;
}

taulukko.sort(vertaa);
// 0: { etunimi: "Tommi", sukunimi: "Lahtonen" }
// 1: { etunimi: "Olli",  sukunimi: "Opiskelija" }
// 2: { 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, vaihtaa palautusarvot 1 ja -1 toisin päin. Entä jos pienillä ja isoilla kirjaimilla ei saa olla merkitystä? Entä mahdolliset välilyönnit etunimen tai sukunimen alussa? Näihin vastaa seuraava luku.

Puuttuvat kentät ja epäyhtenäinen aineisto

Edelliset esimerkit olettavat, että jokaisella alkiolla on kaikki vertailtavat kentät. Oikea JSON-aineisto ei ole näin siisti: kenttä voi puuttua, olla null tai taulukossa voi olla ihan väärän tyyppisiä alkioita. Vertailufunktio ei saa hajota näihin.

undefined-arvon vertailu tuottaa aina false, joten ilman erillistä käsittelyä puuttuvat kentät päätyvät mielivaltaiseen paikkaan. Päätä siis itse, tulevatko ne alkuun vai loppuun, ja kirjoita se ensimmäiseksi:

const vertailija = new Intl.Collator("fi", { sensitivity: "base" });

function vertaa(a, b) {
  // puuttuvat nimet viimeisiksi
  if (!a.nimi && !b.nimi) {
    return 0;
  }
  if (!a.nimi) {
    return 1;
  }
  if (!b.nimi) {
    return -1;
  }
  return vertailija.compare(a.nimi, b.nimi);
}

Huomaa, että molemmat suunnat on käsiteltävä. Pelkkä if (!a.nimi) { return 1; } rikkoo vertailun johdonmukaisuuden: silloin puuttuva nimi häviää kaikelle, myös toiselle puuttuvalle nimelle, ja lopputulos riippuu siitä, missä järjestyksessä selain sattuu alkioita vertailemaan.

Jos taulukossa voi olla muutakin kuin objekteja, suodata roskat pois ennen järjestämistä. Se on selvempää kuin yrittää käsitellä kaikkia tapauksia vertailufunktiossa:

const siistitty = data.filter(alkio => typeof alkio === "object" && alkio !== null);
const jarjestetty = siistitty.toSorted(vertaa);

Objektin muuntaminen merkkijonoksi

Kuten edellä kerrottiin, 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]

Metodin voi ylimääritellä. Järkevin tapa on tehdä se luokassa:

class Opiskelija {
  constructor(etunimi, sukunimi) {
    this.etunimi = etunimi;
    this.sukunimi = sukunimi;
  }
  toString() {
    return `${this.sukunimi} ${this.etunimi}`;
  }
}

const taulukko = [
  new Opiskelija("Oona", "Opiskelija"),
  new Opiskelija("Ölli", "Opiskelija"),
  new Opiskelija("Tommi", "Lahtonen")
];

taulukko.sort();                 // toimii nyt ilman vertailufunktiota
console.log(`${taulukko}`);      // "Lahtonen Tommi,Opiskelija Ölli,Opiskelija Oona"

Huomaa kuitenkin lopputulos: Ölli tuli ennen Oonaa, koska oletusjärjestäminen vertaa Unicode-koodiarvoja eikä osaa suomalaista aakkosjärjestystä. Tämä tapa on siis kätevä mutta riittämätön ääkkösten kanssa — katso seuraava luku.

Samaa toString()-metodia voi kokeilla myös console.logilla:

for (const opis of taulukko) {
  console.log(`${opis}`);        // template-merkkijono pakottaa merkkijonoksi
}

Objektin voi myös antaa vanhan tyylin objektiliteraalissa:

function opisToString() {
  return `${this.sukunimi} ${this.etunimi}`;
}

const taulukko2 = [
  { "etunimi": "Oona", "sukunimi": "Opiskelija", toString: opisToString },
  { "etunimi": "Olli", "sukunimi": "Opiskelija", toString: opisToString }
];

localeCompare() ja Intl.Collator

Merkkijonojen vertailua helpottaa localeCompare(), joka osaa vertailla merkkijonoja kielikohtaisella tavalla. Sen palautusarvot sopivat suoraan sortin vertailufunktioksi.

Tässä täytyy kuitenkin olla tarkkana ja käyttää oikeita parametreja eli määrätä kieleksi suomi ("fi") ja sensitivity-asetukseksi "base", jolloin isoilla ja pienillä kirjaimilla ei ole merkitystä. Oletuksena kieli tulee käyttäjän selaimen asetuksista ja sensitivity on "variant", joka erottelee isot ja pienet kirjaimet.

"ö".localeCompare("z");                            // selaimen kielen mukaan, epävarma
"ö".localeCompare("z", "fi");                      // 1  — ö on z:n jälkeen, oikein
"Ö".localeCompare("ö", "fi", { sensitivity: "base" });  // 0 — samat

Yksinkertaisin vaihtoehto on käyttää Intl.Collator-objektia, joka palauttaa valmiin kielikohtaisen vertailufunktion. Se on myös nopeampi, koska kieliasetukset tulkitaan vain kerran eikä jokaisella vertailulla erikseen:

const vertailija = new Intl.Collator("fi", { sensitivity: "base" });

let taulukko = ["b", "a", "ö", "o"];
taulukko.sort(vertailija.compare);
// ["a", "b", "o", "ö"]

// sama localeComparella
taulukko.sort((a, b) => a.localeCompare(b, "fi", { sensitivity: "base" }));

Numeeriset osat merkkijonoissa saa järjestymään järkevästi numeric-asetuksella:

["kuva10", "kuva2"].sort(new Intl.Collator("fi", { numeric: true }).compare);
// ["kuva2", "kuva10"]   — ilman numeric: ["kuva10", "kuva2"]

Objektit järjestetään aivan samaan tapaan:

const vertailija = new Intl.Collator("fi", { sensitivity: "base" });

const taulukko = [
  { "etunimi": "Oona",  "sukunimi": "Opiskelija" },
  { "etunimi": "Ölli",  "sukunimi": "Opiskelija" },
  { "etunimi": "Tommi", "sukunimi": "Lahtonen" }
];

function vertaa(a, b) {
  const tulos = vertailija.compare(a.sukunimi, b.sukunimi);
  if (tulos !== 0) {
    return tulos;              // sukunimissä oli eroa
  }
  // sukunimet olivat samat, palautetaan etunimien vertailutulos
  return vertailija.compare(a.etunimi, b.etunimi);
}

taulukko.sort(vertaa);
// Lahtonen Tommi, Opiskelija Oona, Opiskelija Ölli   — Oona ennen Öllia, oikein

Esimerkit: locale.html?js=1&console=1, sort2.html?js=1&console=1 sekä sort.html ja sort.js

Päivämäärät ja ajat

Temporal

Vanha Date-objekti on ollut kielen ongelmallisin osa 30 vuoden ajan: se on muuttuva (mutable), sekoittaa keskenään päivämäärän ja aikavyöhykkeen, ja sen kuukaudet alkavat nollasta. Temporal korvaa sen. Temporal hyväksyttiin osaksi ECMAScript 2026 -standardia maaliskuussa 2026.

Temporal on Math-objektin tapainen nimiavaruus, jonka alta löytyy useita tyyppejä eri käyttötarkoituksiin. Kaikki Temporal-oliot ovat muuttumattomia (immutable): laskutoimitukset palauttavat aina uuden olion.

Tyyppi Mihin Esimerkki
Temporal.PlainDate päivämäärä ilman kellonaikaa syntymäpäivä, eräpäivä
Temporal.PlainTime kellonaika ilman päivää aukioloaika
Temporal.PlainDateTime päivä + aika ilman aikavyöhykettä kalenterimerkintä
Temporal.ZonedDateTime tarkka hetki aikavyöhykkeineen kokouksen alku
Temporal.Instant tarkka hetki ilman kalenteria lokimerkintä
Temporal.Duration aikaväli kesto, viive

Nykyhetki

Temporal.Now.plainDateISO();          // 2026-09-07
Temporal.Now.plainDateTimeISO();      // 2026-09-07T13:45:12.345
Temporal.Now.zonedDateTimeISO();      // 2026-09-07T13:45:12.345+03:00[Europe/Helsinki]
Temporal.Now.instant();               // 2026-09-07T10:45:12.345Z
Temporal.Now.timeZoneId();            // "Europe/Helsinki"

Luominen ja osiin pilkkominen

const paiva = Temporal.PlainDate.from("2026-09-07");
const paiva2 = Temporal.PlainDate.from({ year: 2026, month: 9, day: 7 });

paiva.year;          // 2026
paiva.month;         // 9    <-- HUOM: syyskuu on 9, ei 8 kuten Date-objektissa!
paiva.day;           // 7
paiva.dayOfWeek;     // 1 = maanantai
paiva.daysInMonth;   // 30

Kuukausien nollapohjaisuus oli Date-objektin klassinen bugilähde. Temporalissa kuukaudet ovat 1–12 kuten ihmisillä.

Laskutoimitukset

const paiva = Temporal.PlainDate.from("2026-01-31");

paiva.add({ months: 1 });         // 2026-02-28  — ei valu maaliskuun puolelle
paiva.add({ days: 30 });          // 2026-03-02
paiva.subtract({ years: 1 });     // 2025-01-31

// paiva itse ei ole muuttunut:
console.log(paiva.toString());    // "2026-01-31"

// erotus kahden päivän välillä
const alku = Temporal.PlainDate.from("2026-09-01");
const loppu = Temporal.PlainDate.from("2026-12-24");
const ero = alku.until(loppu);              // Temporal.Duration
console.log(ero.days);                      // 114
console.log(alku.until(loppu, { largestUnit: "month" }).toString());  // "P3M23D"

Vertailu ja järjestäminen

Temporal-oliot eivät ole primitiivejä, joten < ja > eivät toimi (ne eivät heitä virhettä vaan palauttavat aina false — hankala bugi). Käytä compare-metodia, joka sopii suoraan sort-funktiolle:

Temporal.PlainDate.compare(alku, loppu);    // -1, 0 tai 1
alku.equals(loppu);                         // false

const paivat = ["2026-12-24", "2026-01-01", "2026-06-21"]
  .map(s => Temporal.PlainDate.from(s));

paivat.sort(Temporal.PlainDate.compare);

Muotoilu näytettäväksi

const p = Temporal.PlainDate.from("2026-09-07");

p.toString();                     // "2026-09-07"  — ISO-muoto, tallennukseen
p.toLocaleString("fi-FI");        // "7.9.2026"    — käyttäjälle näytettäväksi
p.toLocaleString("fi-FI", { dateStyle: "long" });   // "7. syyskuuta 2026"

const hetki = Temporal.Now.zonedDateTimeISO();
hetki.toLocaleString("fi-FI", { dateStyle: "short", timeStyle: "short" });

Sääntö: tallenna ja siirrä ISO-muodossa (toString), näytä lokalisoituna (toLocaleString).

Selaintuki

Temporal on natiivisti tuettu Firefox 139+ (5/2025), Chrome 144+ ja Edge 144+ (1/2026) -selaimissa. Safari ei vielä tue sitä. Jos sovelluksen on toimittava Safarissa, tarvitset polyfillin (temporal-polyfill tai @js-temporal/polyfill). Tarkista ajantasainen tilanne osoitteesta caniuse.com.

Tällä kurssilla Temporalin saa käyttää suoraan, kunhan testaat Firefoxilla tai Chromella.

Lisätietoa: Temporal-dokumentaatio (MDN) ja Temporal cookbook.

Date (vanha rajapinta)

Date-objektia ei kannata enää käyttää uudessa koodissa, mutta sitä näkee kaikkialla, joten sudenkuopat on hyvä tuntea.

const aika = new Date();

aika.getDate();       // kuukauden päivä 1–31
aika.getMonth();      // kuukausi 0–11  <-- NOLLAPOHJAINEN! Tammikuu = 0
aika.getFullYear();   // vuosiluku, esim. 2026
// aika.getYear();    // ÄLÄ KÄYTÄ: vanhentunut, palauttaa vuoden miinus 1900

// päivämäärän tulostus vaatii kuukauteen +1:
console.log(`${aika.getDate()}.${aika.getMonth() + 1}.${aika.getFullYear()}`);

// helpompi ja oikeampi tapa:
console.log(aika.toLocaleDateString("fi-FI"));

Kuluneen ajan mittaus:

// VÄÄRIN: Date-olio jäätyy luontihetkeen, joten erotus on aina 0
// const aika = new Date();
// const alku = aika.getTime();
// tehdään jotain
// const loppu = aika.getTime();   // sama arvo kuin alku!

// OIKEIN: Date.now() palauttaa nykyhetken millisekunteina
const alku = Date.now();
tehdaanJotain();
const loppu = Date.now();
console.log(`Kesti ${loppu - alku} ms`);

// vielä tarkempi ja luotettavampi mittaukseen:
const t0 = performance.now();
tehdaanJotain();
console.log(`Kesti ${performance.now() - t0} ms`);

Huomaa, että kuluneen ajan esittämiseen ei pidä käyttää new Date(erotus) -kikkaa, koska se tulkitsee erotuksen ajanhetkeksi vuodesta 1970 alkaen. Laske suoraan millisekunneista tai käytä Temporal.Duration-tyyppiä.

Tarpeellisia funktioita

Math

Math.abs(-3)        // 3
Math.round(2.5)     // 3     lähimpään kokonaislukuun
Math.floor(2.9)     // 2     alaspäin
Math.ceil(2.1)      // 3     ylöspäin
Math.trunc(-2.9)    // -2    katkaisu kohti nollaa (floor antaisi -3)
Math.min(3, 1, 4)   // 1
Math.max(...luvut)  // taulukon suurin
Math.sqrt(16)       // 4
2 ** 10             // 1024  potenssioperaattori (helpompi kuin Math.pow)

Satunnaisluvut

const sanoja = ["Diiba", "Daaba", "Tesmaus", "Heppa", "Muuli", "Saippuakauppias"];

// Math.random() antaa arvon väliltä [0, 1)
// kertominen pituudella ja pyöristys alaspäin antaa kelvollisen indeksin
const rand = Math.floor(Math.random() * sanoja.length);
console.log(sanoja[rand]);

// satunnainen kokonaisluku väliltä min..max (molemmat mukaan)
function satunnaisluku(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

Math.random() ei sovellu salausavaimiin eikä arpajaisiin. Niihin on crypto.getRandomValues().

Pyöristäminen ja numeroiden muotoilu

Yksinkertainen pyöristys kahteen desimaaliin:

const liukuluku = 3.245200004;
const kaksiDesimaalia = Math.round(liukuluku * 100) / 100;   // 3.25

Tämä on altis liukulukujen epätarkkuudelle (Math.round(1.005 * 100) / 100 antaa 1, ei 1.01), joten näytettävän tekstin muotoiluun se ei ole paras työkalu.

toFixed() palauttaa merkkijonon halutulla desimaalimäärällä, mutta käyttää pistettä desimaalierottimena eikä osaa tuhaterottimia:

(3.2452).toFixed(2);      // "3.25"  — merkkijono, ei numero!

Numeroiden formatointiin ei ole C-kielestä tuttua printf-mahdollisuutta, mutta Intl.NumberFormat tekee sen paremmin ja lokalisoidusti. Se on ainoa oikea tapa muotoilla numeroita käyttäjälle näytettäväksi, koska suomessa desimaalierotin on pilkku ja tuhaterotin on välilyönti:

const muotoilija = new Intl.NumberFormat("fi-FI", {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});
muotoilija.format(1234.5);            // "1 234,50"

// valuutta
new Intl.NumberFormat("fi-FI", { style: "currency", currency: "EUR" })
  .format(1234.5);                    // "1 234,50 €"

// prosentti
new Intl.NumberFormat("fi-FI", { style: "percent" }).format(0.42);   // "42 %"

// etunollat
new Intl.NumberFormat("fi-FI", { minimumIntegerDigits: 2 }).format(5);   // "05"
// merkitsevien numeroiden määrä desimaalien sijaan
const luku = 123.456;
new Intl.NumberFormat("fi-FI", { maximumSignificantDigits: 5 }).format(luku);
// "123,46"
new Intl.NumberFormat("en-US", { maximumSignificantDigits: 5 }).format(luku);
// "123.46"   — sama luku, eri erotinmerkki

// lokaali voi määrätä myös numeromerkit
new Intl.NumberFormat("zh-Hans-CN-u-nu-hanidec").format(luku);
// "一二三.四五六"

Luo muotoilija kerran silmukan ulkopuolella ja käytä sitä uudelleen; sen luominen on suhteellisen raskasta.

Katso myös Intl.DateTimeFormat, Intl.ListFormat ja Intl.RelativeTimeFormat.

Number.isNaN

Tutkii, onko arvo erikoisarvo NaN. NaN ei ole yhtä suuri kuin mikään, ei edes itsensä, joten ===-vertailu ei toimi.

const numero = Number(syote);
if (Number.isNaN(numero)) {
  console.log("Syöte ei ollut kelvollinen luku");
}

Käytä Number.isNaN-metodia äläkä vanhaa globaalia isNaN-funktiota. Globaali versio muuntaa argumentin ensin numeroksi, mikä antaa yllättäviä tuloksia:

isNaN("hei");           // true  — muunnos epäonnistui
isNaN("");              // false — tyhjä merkkijono muuntuu nollaksi
isNaN(undefined);       // true

Number.isNaN("hei");    // false — merkkijono "hei" EI ole NaN-arvo
Number.isNaN(NaN);      // true

typeof

Palauttaa merkkijonona arvon tyypin.

typeof 42               // "number"
typeof "teksti"         // "string"
typeof true             // "boolean"
typeof undefined        // "undefined"
typeof Symbol()         // "symbol"
typeof 10n              // "bigint"
typeof function () {}   // "function"
typeof {}               // "object"
typeof []               // "object"   <-- taulukkoa ei erota näin!
typeof null             // "object"   <-- kielen bugi, katso null ja undefined

Taulukon tunnistamiseen käytetään Array.isArray(x) ja luokan ilmentymän tunnistamiseen x instanceof Luokka.

Moduulit

Kun ohjelma kasvaa, jaa se useaan tiedostoon ES-moduuleilla. Jokainen moduuli on oma näkyvyysalueensa, joten globaalit muuttujat eivät sotke toisiaan.

apuvalineet.js:

export function summaa(a, b) {
  return a + b;
}

export const VERSIO = "1.0";

// yksi oletusvienti per moduuli
export default class Laskin { ... }

main.js:

import Laskin, { summaa, VERSIO } from "./apuvalineet.js";

console.log(summaa(1, 2));

HTML:ssä moduuli otetaan käyttöön type="module"-attribuutilla:

<script src="main.js" type="module"></script>

Huomioitavaa:

Asynkroninen JavaScript

Verkkopyynnöt ja muut hitaat operaatiot eivät pysäytä selainta odottamaan vaan palauttavat Promise-objektin, joka valmistuu myöhemmin. Näihin palataan tarkemmin TIES4080-kurssilla, mutta perusmuoto on hyvä tunnistaa:

async function haeData() {
  try {
    const vastaus = await fetch("data.json");
    if (!vastaus.ok) {
      throw new Error(`HTTP-virhe ${vastaus.status}`);
    }
    const data = await vastaus.json();
    return data;
  }
  catch (virhe) {
    console.error("Haku epäonnistui:", virhe.message);
    return null;
  }
}

await toimii vain async-funktion sisällä (tai moduulin ylimmällä tasolla). Muista, että async-funktio palauttaa aina Promisen.

Katso Using promises.

Lisälukemista

Käyttäjien kommentit

Kommentoi Lisää kommentti