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ä.
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.
- Javascript ei liity millään tavalla Java-ohjelmointikieleen.
- Javascript on dynaamisesti tyypitetty ja heikosti tyypitetty. Nämä ovat eri asioita, ja jälkimmäinen aiheuttaa suurimman osan yllätyksistä; katso luku Tyypityksen termit.
- Jos haluat staattisen tyyppitarkistuksen, katso TypeScript tai JSDoc-kommentit.
- Javascript on prototyyppipohjainen kieli eikä luokkapohjainen kuten esim. Java.
class-syntaksi on prototyyppien päälle rakennettu helpotus. - Javascript-tyyliopas
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.
Selaimen kehittäjätyökalut
Firefoxin ja Chromen kehittäjätyökalut (F12) ovat tärkein työkalusi:
- Web Console — virheilmoitukset ja
console.log-tulosteet. Konsoliin voi myös kirjoittaa ja ajaa koodia suoraan. - Konsolin monirivitila (Ctrl+B tai nuolipainike konsolin oikeassa laidassa) on kokeiluihin paljon kätevämpi kuin yhden rivin kehote: siinä voi kirjoittaa kokonaisen funktion, ajaa sen ja muokata koodia uudelleen ilman että kaikki pitää kirjoittaa alusta.
- Debugger — koodin askellus ja breakpointit. Opettele tämä; se säästää enemmän aikaa kuin mikään muu yksittäinen taito.
- Inspector — sivun elementit ja tyylit.
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:
- Jokainen elementti on suljettava.
</li>,</p>ja</td>eivät ole valinnaisia. - Tyhjät elementit itsesuljetaan:
<br />,<img ... />,<meta ... />,<input ... />. - Ei-tyhjää elementtiä ei koskaan itsesuljeta.
<script src="x.js" />ja<div />rikkoutuvat HTML-jäsentimessä; kirjoita<script src="x.js"></script>. - Kaikki attribuuttien arvot lainausmerkeissä, myös numerot:
colspan="2". - XML:ssä ei ole totuusarvoattribuutteja, joten kirjoita
checked="checked",selected="selected",disabled="disabled",required="required",defer="defer". - XML tuntee vain viisi nimettyä entiteettiä:
&,<,>,"ja'. Kaikki muut, kuten jaä, rikkovat sivun. Käytä numeerista viittausta ( ) tai kirjoita merkki suoraan UTF-8-tiedostoon (ä,©). - Osoitteissa oleva
&on kirjoitettava muodossa&:<a href="haku?a=1&b=2">.
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:
type="text/javascript"on tarpeeton. HTML5:ssä JavaScript on oletustyyppi. Attribuutti ei riko mitään, mutta sitä ei enää kirjoiteta.- Koska kurssilla käytetään XHTML:ää,
script-elementtiä ei saa sulkea muodossa<script src="x.js" />vaan aina<script ...></script>.
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ä
- Isoilla ja pienillä kirjaimilla on merkitystä (
Fooon eri kuinfoo). - Merkistönä käytetään Unicodea. Tallenna lähdekoodi aina UTF-8-muodossa.
- Lauseet päättyvät puolipisteeseen (
;). Javascript ei kaikissa tilanteissa kaipaa puolipistettä, koska se osaa lisätä niitä automaattisesti (ASI), mutta automatiikka menee joskus pieleen. On erittäin suositeltavaa käyttää puolipistettä aina! - Kommentit merkitään kuten C++:ssa:
//ja/* */.
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
NaN— Not-A-Number, epäonnistuneen laskutoimituksen tulos.
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.
Infinityja-Infinity— äärettömyydet. Huomaa, että1 / 0ei ole virhe vaanInfinity.
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:
| Kysymys | Vaihtoehdot | Javascript |
|---|---|---|
| Kirjoitetaanko tyyppi lähdekoodiin? | staattinen / dynaaminen | dynaaminen — tyyppi selviää arvosta ajon aikana |
| Muuntaako kieli tyyppejä itsestään? | vahva / heikko | heikko — 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
- Matemaattisia operaatioita varten on globaali objekti
Math. typeofpalauttaa merkkijonon, joka kertoo arvon tyypin.inpalauttaatrue, jos kysytty ominaisuus löytyy objektista:"nimi" in henkilo.instanceoftestaa, onko objekti tietyn luokan ilmentymä.newluo uusia objekteja.deletepoistaa objektin ominaisuuden. Se ei poista muuttujia eikä objekteja, eikä sitä pidä käyttää taulukoihin (katso luku Delete vs. splice).
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:
| n | sataa | paljon | lämmin | pyöräilemään | muuta |
|---|---|---|---|---|---|
| 0 | F | F | F | T | |
| 1 | F | F | T | T | |
| 2 | F | T | F | T | ei mahdollinen |
| 3 | F | T | T | T | ei mahdollinen |
| 4 | T | F | F | F | |
| 5 | T | F | T | T | |
| 6 | T | T | F | F | |
| 7 | T | T | T | F |
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:
- Difference between for…of and for…in
- Enumerability and ownership of properties
- Why is using “for…in” for array iteration a bad idea?
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
Taulukot
Taulukko luodaan seuraavasti:
const paikat = [];
paikat[0] = "Jämsä";
paikat[1] = "Äänekoski";
tai suoraan:
const paikat = ["Jämsä", "Äänekoski"];
- Taulukon indeksinä on aina numero, ja indeksointi alkaa nollasta.
taulukko.lengthon ominaisuus (ei metodi), ja sen arvo on suurin käytetty indeksi + 1.- Taulukko on itse asiassa objekti, jolle on määritelty
lengthja taulukkometodit.
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:
- Avaimet on aina kirjoitettava kaksinkertaisiin lainausmerkkeihin:
{"nimi": "x"}, ei{nimi: "x"}. - Merkkijonoissa käytetään vain kaksinkertaisia lainausmerkkejä, ei heittomerkkejä.
- Ei kommentteja.
- Ei perässä roikkuvia pilkkuja (
[1, 2, 3,]on virheellistä JSONia). - Ei arvoja
undefined,NaNeikäInfinity. - Ei funktioita, ei
Date-,Map- eikäSet-objekteja.
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.
- osoitin funktioon:
vertaaLuvut - funktiokutsu:
vertaaLuvut()
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:
- Moduuli suoritetaan vasta, kun koko dokumentti on ladattu, joten latausjärjestystä ei tarvitse erikseen miettiä.
- Moduulit ovat aina strict modessa;
"use strict"on tarpeeton. - Polun on oltava suhteellinen ja päätteen mukana (
"./apuvalineet.js", ei"apuvalineet"). - Moduulit eivät toimi
file://-osoitteella, vaan tarvitset WWW-palvelimen (esim. VS Coden Live Server).
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
- MDN JavaScript Guide
- Eloquent JavaScript — kokonainen kirja verkossa, jonka esimerkit ovat suoraan ajettavissa ja muokattavissa selaimessa
- You Don’t Know JS Yet — Get Started (2nd ed.)
- JavaScript.info — laaja ja ajantasainen oppimateriaali
- Airbnb JavaScript Style Guide
- ECMAScript-standardin uusin luonnos
Käyttäjien kommentit