b

Javascriptiä

Kurssin aikana on web-sovelluskehityksen rinnalla tavoite ja tarve oppia riittävässä määrin JavaScriptiä.

JavaScript on kehittynyt viime vuosina nopeaan tahtiin, ja käytämme kurssilla kielen uusimpien versioiden piirteitä. JavaScript-standardin virallinen nimi on ECMAScript. Tämän hetken tuorein versio on kesäkuussa 2020 julkaistu ES11, toiselta nimeltään ECMAScript 2020.

Selaimet eivät vielä osaa kaikkia JavaScriptin uusimpien versioiden ominaisuuksia. Tämän takia selaimessa suoritetaan useimmiten koodia, joka on käännetty (englanniksi transpiled) uudemmasta JavaScriptin versiosta johonkin vanhempaan, laajemmin tuettuun versioon.

Tällä hetkellä johtava tapa tehdä transpilointi on Babel. Create-react-app:in avulla luoduissa React-sovelluksissa on valmiiksi konfiguroitu automaattinen transpilaus. Katsomme kurssin osassa 7 tarkemmin miten transpiloinnin konfigurointi tapahtuu.

Node.js on melkein missä vaan (mm. palvelimilla) toimiva, Googlen Chrome V8-JavaScript-moottoriin perustuva JavaScript-suoritusympäristö. Harjoitellaan hieman JavaScriptiä Nodella. Tässä oletetaan, että koneellasi on Node.js:stä vähintään versio 14.8.0. Noden tuoreet versiot osaavat suoraan JavaScriptin kohtuullisen uusia versioita, joten koodin transpilaus ei ole tarpeen.

Koodi kirjoitetaan .js-päätteiseen tiedostoon ja suoritetaan komennolla node tiedosto.js

Koodia on mahdollista kirjoittaa myös Node.js-konsoliin, joka aukeaa kun kirjoitat komentorivillä node tai myös selaimen developer toolin konsoliin. Chromen uusimmat versiot osaavat suoraan transpiloimatta melko hyvin JavaScriptin uusiakin piirteitä.

JavaScript muistuttaa nimensä ja syntaksinsa puolesta läheisesti Javaa. Perusmekanismeiltaan kielet kuitenkin poikkeavat radikaalisti. Java-ohjelmoijalle JavaScriptin käyttäytyminen saattaa aiheuttaa hämmennystä, erityisesti jos kielen piirteistä ei viitsi ottaa selvää.

Tietyissä piireissä on myös ollut suosittua yrittää "simuloida" JavaScriptilla eräitä Javan tai Pythonin piirteitä ja ohjelmointitapoja, mutta se ei ole suositeltavaa.

Muuttujat

JavaScriptissä on muutama tapa määritellä muuttujia:

const x = 1
let y = 5

console.log(x, y)   // tulostuu 1, 5
y += 10
console.log(x, y)   // tulostuu 1, 15
y = 'teksti'
console.log(x, y)   // tulostuu 1, teksti
x = 4               // aiheuttaa virheen

const ei oikeastaan määrittele muuttujaa vaan vakion, jonka arvoa ei voi enää muuttaa. let taas määrittelee normaalin muuttujan.

Muuttujan tallettaman tiedon tyyppi voi vaihtua suorituksen aikana, y tallettaa aluksi luvun ja lopulta merkkijonon.

JavaScriptissa on myös mahdollista määritellä muuttujia avainsanan var avulla. Var oli pitkään ainoa tapa muuttujien määrittelyyn, const ja let tulivat kieleen mukaan vasta versiossa ES6. Var toimii tietyissä tilanteissa eri tavalla kuin useimpien muiden kielien muuttujien määrittely. Tällä kurssilla varin käyttö ei ole suositeltavaa eli käytä aina const:ia tai let:iä!

Lisää aiheesta on esim. YouTubessa: var, let and const - ES6 JavaScript Features

Taulukot

Taulukko ja muutama esimerkki sen käytöstä:

const t = [1, -1, 3]

t.push(5)

console.log(t.length) // tulostuu 4
console.log(t[1])     // tulostuu -1

t.forEach(value => {
  console.log(value)  // tulostuu 1, -1, 3, 5 omille riveilleen
})                    

Huomaa, että taulukon sisältöä voi muuttaa, vaikka taulukko on määritelty const:ksi. Koska taulukko on olio, muuttuja viittaa koko ajan samaan olioon, jonka sisältö muuttuu kun taulukkoon lisätään uusia alkioita.

Eräs tapa käydä taulukon alkiot läpi on esimerkissä käytetty forEach, joka saa parametrikseen nuolisyntaksilla määritellyn funktion

value => {
  console.log(value)
}

forEach kutsuu funktiota jokaiselle taulukon alkiolle antaen taulukon yksittäisen alkion yksi kerrallaan funktiolle parametrina. forEachin parametrina oleva funktio voi saada myös muita parametreja.

Edellisessä esimerkissä taulukkoon lisättiin uusi alkio metodilla push. Reactin yhteydessä sovelletaan usein funktionaalisen ohjelmoinnin tekniikoita, ja eräs piirre on käyttää muuttumattomia (engl. immutable) tietorakenteita. React-koodissa kannattaakin mieluummin käyttää metodia concat, joka ei lisää alkiota taulukkoon vaan luo uuden taulukon, jossa on lisättävä alkio sekä vanhan taulukon sisältö:

const t = [1, -1, 3]

const t2 = t.concat(5)

console.log(t)  // tulostuu [1, -1, 3]
console.log(t2) // tulostuu [1, -1, 3, 5]

Metodikutsu t.concat(5) ei siis lisää uutta alkiota vanhaan taulukkoon, vaan palauttaa uuden taulukon, joka sisältää vanhan taulukon alkioiden lisäksi uuden alkion.

Taulukoille on määritelty runsaasti hyödyllisiä operaatioita. Katsotaan pieni esimerkki metodin map käytöstä:

const t = [1, 2, 3]

const m1 = t.map(value => value * 2)
console.log(m1)   // tulostuu [2, 4, 6]

Map muodostaa taulukon perusteella uuden taulukon, jonka jokainen alkio luodaan map:in parametrina olevan funktion avulla, esimerkin tapauksessa kertomalla alkuperäinen luku kahdella.

Map voi muuttaa taulukon myös täysin erilaiseen muotoon:

const m2 = t.map(value => '<li>' + value + '</li>')
console.log(m2)  
// tulostuu [ '<li>1</li>', '<li>2</li>', '<li>3</li>' ]

Yllä lukuja sisältävästä taulukosta tehdään map-metodin avulla HTML-koodia sisältävä taulukko. Tulemmekin kurssin osassa2 näkemään, että mapia käytetään Reactissa todella usein.

Taulukon yksittäisiä alkioita on helppo sijoittaa muuttujiin destrukturoivan sijoituslauseen avulla:

const t = [1, 2, 3, 4, 5]

const [first, second, ...rest] = t

console.log(first, second)  // tulostuu 1, 2
console.log(rest)          // tulostuu [3, 4 ,5]

Yllä muuttujiin first ja second sijoitetaan taulukon kaksi ensimmäistä lukua. Muuttujaan rest "kerätään" sijoituksesta jäljelle jääneet luvut omaksi taulukoksi.

Oliot

JavaScriptissä on muutama tapa määritellä olioita. Erittäin yleisesti käytetään olioliteraaleja, eli määritellään olio luettelemalla sen kentät (englanniksi property) aaltosulkeiden sisällä:

const object1 = {
  name: 'Arto Hellas',
  age: 35,
  education: 'Filosofian tohtori',
}

const object12 = {
  name: 'Full Stack -websovelluskehitys',
  level: 'aineopinto',
  size: 5,
}

const object3 = {
  name: {
    first: 'Juha',
    last: 'Tauriainen',
  },
  grades: [2, 3, 5, 3],
  department: 'TKTL',
}

Kenttien arvot voivat olla tyypiltään mitä vaan: lukuja, merkkijonoja, taulukoita, olioita...

Olioiden kenttiin viitataan pistenotaatiolla tai hakasulkeilla:

console.log(object1.name)         // tulostuu Arto Hellas
const fieldName = 'age' 
console.log(object1[fieldName])    // tulostuu 35

Olioille voidaan lisätä kenttiä myös lennossa joko pistenotaation tai hakasulkeiden avulla:

object1.address = 'Tapiola'
object1['secret number'] = 12341

Jälkimmäinen lisäyksistä on pakko tehdä hakasulkeiden avulla, sillä pistenotaatiota käytettäessä secret number ei kelpaa kentän nimeksi.

JavaScriptissä olioilla voi olla myös metodeja. Tällä kurssilla emme kuitenkaan tarvitse itse määriteltyjä metodillisia olioita, joten asiaa ei käsitellä kuin lyhyesti.

Olioita on mahdollista määritellä myös ns. konstruktorifunktioiden avulla, jolloin saadaan aikaan hieman monien muiden ohjelmointikielten, esim. Javan tai Pythonin luokkia (class) muistuttava mekanismi. JavaScriptissä ei kuitenkaan ole luokkia samassa mielessä kuin olio-ohjelmointikielissä. Kieleen on kuitenkin lisätty versiosta ES6 alkaen luokkasyntaksi, joka helpottaa tietyissä tilanteissa olio-ohjelmointikielimäisten luokkien esittämistä.

Funktiot

Olemme jo tutustuneet ns. nuolifunktioiden määrittelyyn. Täydellinen eli "pitkän kaavan" mukaan menevä tapa nuolifunktion määrittelyyn on seuraava

const sum = (p1, p2) => {
  console.log(p1)
  console.log(p2)
  return p1 + p2
}

ja funktiota kutsutaan kuten olettaa saattaa:

const result = sum(1, 5)
console.log(result)

Jos parametreja on vain yksi, sulut voidaan jättää määrittelystä pois:

const square = p => {
  console.log(p)
  return p * p
}

Jos funktio sisältää ainoastaan yhden lausekkeen, ei aaltosulkeita tarvita. Tällöin funktio palauttaa ainoan lausekkeensa arvon. Eli jos poistetaan konsoliin tulostus, voidaan edellinen funktio ilmaista lyhyemmin seuraavasti:

const square = p => p * p

Tämä muoto on erityisen kätevä käsiteltäessä taulukkoja esim. map-metodin avulla:

const t = [1, 2, 3]
const tSquared = t.map(p => p * p)
// tSquared on nyt [1, 4, 9]

Nuolifunktio on tullut Javascriptiin vasta muutama vuosi sitten version ES6 myötä. Tätä ennen ainoa tapa funktioiden määrittelyyn oli avainsanan function käyttö.

Määrittelytapoja on kaksi, funktiolle voidaan antaa function declaration -tyyppisessä määrittelyssä nimi, jonka avulla funktioon voidaan viitata:

function product(a, b) {
  return a * b
}

const vastaus = product(2, 6)

Toinen tapa on tehdä määrittely funktiolausekkeena. Tällöin funktiolle ei tarvitse antaa nimeä ja määrittely voi sijaita muun koodin seassa:

const average = function(a, b) {
  return (a + b) / 2
}

const vastaus = average(2, 5)

Määrittelemme tällä kurssilla kaikki funktiot nuolisyntaksin avulla.

Olioiden metodit ja this

Koska käytämme tällä kurssilla Reactin hookit sisältävää versiota, meidän ei kurssin aikana tarvitse määritellä ollenkaan olioita, joilla on metodeja. Tämän luvun asiat siis eivät ole kurssin kannalta relevantteja, mutta varmasti monella tapaa hyödyllisiä tietää. Käytettäessä "vanhempaa Reactia" tämän luvun asiat on hallittava.

Nuolifunktiot ja avainsanan function avulla määritellyt funktiot poikkeavat radikaalisti siinä miten ne käyttäytyvät olioon itseensä viittaavan avainsanan this suhteen.

Voimme liittää oliolle metodeja määrittelemällä niille kenttiä, jotka ovat funktioita:

const arto = {
  name: 'Arto Hellas',
  age: 35,
  education: 'Filosofian tohtori',
  greet: function() {
    console.log('hello, my name is', this.name)
  },
}

arto.greet()  // tulostuu hello, my name is Arto Hellas

Metodin sisällä voidaan siis viitata olion kenttien arvoihin avainsanan this avulla vastaavasti kuin Javassa. Pythonissa saman asian ajaa avainsana self.

Metodeja voi lisätä myös olion luomisen jälkeen:

const arto = {
  name: 'Arto Hellas',
  age: 35,
  education: 'Filosofian tohtori',
  greet: function() {
    console.log('hello, my name is', this.name)
  },
}

arto.growOlder = function() {  this.age += 1}
console.log(arto.age)   // tulostuu 35
arto.growOlder()
console.log(arto.age)   // tulostuu 36

Muutetaan oliota hiukan:

const arto = {
  name: 'Arto Hellas',
  age: 35,
  education: 'Filosofian tohtori',
  greet: function() {
    console.log('hello, my name is', this.name)
  },
  doAddition: function(a, b) {    console.log(a + b)  },}

arto.doAddition(1, 4)        // tulostuu 5

const referenceToAddition = arto.doAddition
referenceToAddition(10, 15)  // tulostuu 25

Oliolla on nyt metodi doAddition, joka osaa laskea parametrina annettujen lukujen summan. Metodia voidaan kutsua normaaliin tapaan olion kautta arto.doAddition(1, 4) tai tallettamalla metodiviite muuttujaan ja kutsumalla metodia muuttujan kautta referenceToAddition(10, 15).

Jos yritämme samaa metodille greet, aiheutuu ongelmia:

arto.greet()       // tulostuu hello, my name is Arto Hellas

const referenceToGreet = arto.greet
referenceToGreet() // tulostuu ainoastaan hello, my name is

Kun metodia kutsutaan viitteen kautta, metodi on kadottanut tiedon siitä, mikä alkuperäinen this oli. Toisin kuin melkein kaikissa muissa kielissä, JavaScriptissa this:n arvo määrittyy sen mukaan miten metodia on kutsuttu. Kutsuttaessa metodia viitteen kautta, this:in arvoksi tulee ns. globaali objekti ja lopputulos ei ole yleensä ollenkaan se, mitä sovelluskehittäjä olettaa.

This:in kadottaminen saattaa aiheuttaa ongelmia. Eteen tulee usein tilanteita, joissa Reactin/Noden (tai oikeammin ilmaistuna selaimen JavaScript-moottorin) tulee kutsua joitain ohjelmoijan määrittelemien olioiden metodeja. Tällä kurssilla kuitenkin säästymme näiltä ongelmilta, sillä käytämme ainoastaan "thissitöntä" JavaScriptiä.

this katoaa esimerkiksi, jos pyydetään Artoa tervehtimään sekunnin kuluttua metodia setTimeout käyttäen:

const arto = {
  name: 'Arto Hellas',
  greet: function() {
    console.log('hello, my name is', this.name)
  },
}

setTimeout(arto.greet, 1000)// sekunnin päästä tulostuu hello, my name is

JavaScriptissa this:in arvo siis määräytyy siitä miten metodia on kutsuttu. setTimeoutia käytettäessä metodia kutsuu JavaScript-moottori, ja this viittaa Timeout-olioon.

On useita mekanismeja, joiden avulla alkuperäinen this voidaan säilyttää, eräs näistä on metodin bind käyttö:

setTimeout(arto.greet.bind(arto), 1000)
// sekunnin päästä tulostuu hello, my name is Arto Hellas

Komento arto.greet.bind(arto) luo uuden funktion, jossa this on sidottu tarkoittamaan Artoa riippumatta siitä, missä ja miten metodia kutsutaan.

Nuolifunktioiden avulla on mahdollista ratkaista eräitä this:iin liittyviä ongelmia. Olioiden metodeina niitä ei kuitenkaan kannata käyttää, sillä silloin this ei toimi ollenkaan.

Jos haluat ymmärtää paremmin JavaScriptin this:in toimintaa, Internetissä on runsaasti materiaalia aiheesta. Esim. egghead.io:n 20 minuutin screencast-sarja Understand JavaScript's this Keyword in Depth on erittäin suositeltava!

Luokat

Kuten aiemmin mainittiin, JavaScriptissä ei ole olemassa olio-ohjelmointikielten luokkamekanismia. JavaScriptissa on kuitenkin ominaisuuksia, jotka mahdollistavat olio-ohjelmoinnin luokkien "simuloinnin". Emme mene nyt sen tarkemmin JavaScriptin olioiden taustalla olevaan prototyyppiperintämekanismiin.

Tutustutaan nyt pikaisesti ES6:n myötä JavaScriptiin tulleeseen luokkasyntaksiin, joka helpottaa oleellisesti luokkien (tai luokan kaltaisten asioiden) määrittelyä JavaScriptissa.

Seuraavassa on määritelty "luokka" Person ja kaksi Person-oliota:

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  greet() {
    console.log('hello, my name is', this.name)
  }
}

const arto = new Person('Arto Hellas', 35)
arto.greet()

const juhq = new Person('Juha Tauriainen', 48)
juhq.greet()

Syntaksin osalta luokat ja niistä luodut oliot muistuttavat erittäin paljon esim. Javan luokkia ja olioita. Käyttäytymiseltäänkin ne ovat aika lähellä Javan olioita. Kyse on kuitenkin edelleen JavaScriptin prototyyppiperintään perustuvista olioista. Molempien olioiden todellinen tyyppi on Object sillä JavaScriptissä ei ole muita tyyppejä kuin Boolean, Null, Undefined, Number, String, Symbol, BigInt ja Object.

Luokkasyntaksin tuominen JavaScriptiin on osin kiistelty lisäys, kts. esim. Not Awesome: ES6 Classes tai Is “Class” In ES6 The New “Bad” Part?.

ES6:n luokkasyntaksia käytetään paljon "vanhassa" Reactissa ja Node.js:ssä ja siksi sen tunteminen on tälläkin kurssilla paikallaan. Koska käytämme kurssilla Reactiin vuonna 2019 lisättyä hook-ominaisuutta, meidän ei ole tarvetta käyttää kurssilla ollenkaan JavaScriptin luokkasyntaksia.

JavaScript-materiaalia

JavaScriptistä löytyy verkosta suuret määrät sekä hyvää että huonoa materiaalia. Tällä sivulla lähes kaikki JavaScriptin ominaisuuksia käsittelevät linkit ovat Mozillan JavaScript -materiaaliin.

Mozillan sivuilta kannattaa lukea oikeastaan välittömästi A re-introduction to JavaScript (JS tutorial).

Jos haluat tutustua JavaScriptiin syvällisesti, Internetistä on ilmaiseksi mainio kirjasarja You-Dont-Know-JS.

Toinen hieno sivusto JavaScriptin oppimiseen on javascript.info.

egghead.io:ssa on tarjolla laadukkaita screencasteja JavaScriptista, Reactista ym. kiinnostavasta. Valitettavasti materiaali on osittain maksullista.