Stödjande inspelningar
Kurslitteraturen är en av de delar som driver kursen framåt, men det kan vara svårt att själv strukturera upp innehållet. Vad är relevant? Vad bör du fokusera på i kursen? De stödjande inspelningarna tas fram av kursledningen för att just ge detta sammanhang. Det är av yttersta vikt att du tar del av dessa!
Kursvecka 1 - Sekvenser
Section titled “Kursvecka 1 - Sekvenser”Typer, värden, uttryck och operatorer
Section titled “Typer, värden, uttryck och operatorer”Nu är det dags att titta på vad som döljer sig bakom uttryck så som typer, värden, uttryck och operatorer.
Allt är ettor och nollor
Section titled “Allt är ettor och nollor”All data i en dator lagras binärt – som en lång ström av ettor och nollor. För att ge den strömmen mening delas den in i värden av olika typer. Textsträngen 'Hej hopp!' lagras exempelvis som ASCII-värden (72, 101, 106 …) vilka i sin tur är binära tal.
Datatyper i JavaScript
Section titled “Datatyper i JavaScript”JavaScript är ett löst typat språk, men det saknar inte typer. Det finns åtta typer totalt: sju primitiva och en objekttyp.
Primitiva typer: Number, String, Boolean, Undefined, Null, BigInt och Symbol.
Objekttypen: Object – en osorterad samling av namn/värde-par. Arrayer och Date är exempel på inbyggda objekttyper baserade på Object.
De typer man möter mest i början är Number, String och Boolean. Operatorn typeof avslöjar vilken typ ett värde har – men den har ett känt undantag: typeof null returnerar 'object' trots att null är ett primitivt värde. Det är en bugg som funnits sedan språkets tillkomst och som aldrig rättats av hänsyn till bakåtkompatibilitet.
Number
Section titled “Number”Alla tal lagras som 64-bitars flyttal enligt IEEE 754-standarden, vilket ger drygt 18 triljoner möjliga värden. Tal kan uttryckas som heltal, decimaltal eller på exponentiell form. Från och med ES2021 kan understreck användas som tusentalsavgränsare: 1_000_000.
Aritmetiska operatorer fungerar som i vanlig matematik, inklusive prioritetsordning – multiplikation går före addition, och parenteser har högst prioritet. Modulusoperatorn (%) ger resten vid division och är användbar för att avgöra om ett tal är jämnt eller udda.
Decimaltal kan inte alltid representeras exakt i binärt format, vilket kan ge oväntade resultat – exempelvis är 0.1 + 0.2 !== 0.3 i JavaScript. Värt att hålla i minnet vid beräkningar.
Tre specialvärden finns: Infinity, -Infinity och NaN (Not-A-Number). Det sistnämnda är “giftigt”: ingår NaN i en beräkning blir hela resultatet NaN. Kontrollera alltid indata noga för att undvika att NaN sprider sig i uträkningar.
String
Section titled “String”Strängar används för att representera text och kan skrivas inom apostrofer, citattecken eller grav accent (backticks). I den här kursen gäller apostrofer – kursens kodstandard förbjuder citattecken.
Strängar kan läggas ihop med + (konkatenering). Escape-sekvensen \n infogar en radbrytning i strängen.
Boolean
Section titled “Boolean”Booleska värden har bara två möjliga värden: true eller false. De uppstår ofta som resultat av jämförelser. Använd alltid strikt jämförelse med tre likhetstecken (===) – aldrig ==, som tillåter automatisk typkonvertering och kan ge oväntade resultat.
Logiska operatorer kombinerar booleska värden: && (och), || (eller) och ! (icke).
Objekt
Section titled “Objekt”Allt som inte är ett primitivt värde är ett objekt. Ett objekt är en samling egenskaper där varje egenskap har ett namn och ett värde. Arrayer är en typ av objekt för ordnade samlingar – lämpligast när alla värden är av samma typ.
Uttryck och satser
Section titled “Uttryck och satser”Ett uttryck är kod som producerar ett värde. En sats är en komplett instruktion, uppbyggd av ett eller flera uttryck – ungefär som en mening består av ord. Satser tillsammans bildar ett program.
I kursen används inte semikolon efter satser – kodstandarden kräver att de utelämnas.
Kursvecka 2 - Funktioner och moduler
Section titled “Kursvecka 2 - Funktioner och moduler”Variabler och konstanter
Section titled “Variabler och konstanter”Variabler och konstanter är några av de absolut viktigaste byggstenarna i programmering. Hur skapar du dessa och vad ska du tänka på när det kommer till namngivning?
Varför variabler?
Section titled “Varför variabler?”Hittills har vi skrivit lösa uttryck – 12 + 13 ger 25, men vi kan inte jobba vidare med det värdet. Variabler löser det: de låter programmet komma ihåg värden genom att binda ett namn till ett värde. Vi säger att vi tilldelar variabeln ett värde.
En variabel deklareras med nyckelordet let följt av ett namn. Sedan ES6 (2015) är let standarden – tidigare användes var, som fortfarande förekommer i äldre kod och har väsentliga skillnader mot let (bl.a. i hur scope och hoisting fungerar).
let count // undefined – inget värde tilldelat ännulet result = (4 + 8) * 3 + 8 // 44Om ingen initiering görs får variabeln automatiskt värdet undefined. När ett värde väl är tilldelat kan det ändras, och variabeln kan användas i nya uttryck:
let count = 4 * 12 // 48count = count + 12 // 60JavaScript är löst typat, vilket betyder att en variabel kan tilldelas ett värde av en annan typ än den ursprungliga. Det är möjligt men sällan en bra idé – välj istället separata variabler med tydliga namn.
Namngivning
Section titled “Namngivning”Bra variabelnamn är centralt. Håll dig till engelska, välj beskrivande namn, och tillämpa camelCasing när namnet består av flera ord – varje ord utom det första inleds med versal:
let bestBookEver = 'The Hitchhiker\'s Guide to the Galaxy'let theMeaningOfLife = 42Tillåtna tecken är a–z, A–Z, 0–9, _ och $. Första tecknet får inte vara en siffra. I praktiken håller man sig till a–z och A–Z. Namn får inte vara nyckelord eller reserverade ord (som let, const, class, return m.fl.) – annars uppstår syntaxfel.
Utöver camelCasing finns PascalCasing (versalt första bokstav i varje ord, används senare för klasser) och snake_casing (understreck mellan ord, vanligt i t.ex. Python). JavaScript-konventionen och kursens kodstandard kräver camelCasing.
Deklarera en variabel per rad – det underlättar läsbarhet och dokumentation.
Konstanter
Section titled “Konstanter”En konstant fungerar som en variabel med två skillnader: den måste ges ett värde när den deklareras, och värdet kan sedan inte ändras. Försök att ändra det kastar ett TypeError.
const firstName = 'Johan'firstName = 'Mats' // TypeError: Assignment to constant variable.Däremot går det bra att använda värdet för att skapa nya värden:
const firstName = 'Johan'const fullName = firstName + ' Doe' // Skapar en ny sträng – ändrar inte firstNameDet här fungerar eftersom strängar är immutabla – man kan inte ändra en befintlig sträng, bara skapa nya utifrån den.
Namngivning av konstanter diskuteras i kurslitteraturen: vissa föredrar MY_FAVORITE_NUMBER (versaler med understreck) för värden som är hårdkodade och aldrig förändras, medan camelCasing används för konstanter som tilldelas dynamiskt vid körning. I kursen används camelCasing genomgående för enkelhetens skull.
Räckvidd (scope)
Section titled “Räckvidd (scope)”En variabels scope är det kodblock där den kan användas. Variabler och konstanter deklarerade med let och const har block scope – de existerar bara inom de klammerparenteser där de skapades.
{ let bestBookEver = 'The Hitchhiker\'s Guide to the Galaxy' console.log(bestBookEver) // Fungerar}console.log(bestBookEver) // ReferenceError: bestBookEver is not definedDet är möjligt att ha variabler med samma namn i ett yttre och ett inre block – de är då två separata variabler. Inom ett och samma block däremot kan ett namn bara deklareras en gång.
Observera att klammerparenteser i JavaScript har dubbel betydelse: de skapar block, men används också för objektliteraler ({ name: 'Johan' }). Sammanhanget avgör tolkningen, och det lär man sig snabbt.
Funktioner
Section titled “Funktioner”För att kunna dela upp din kod i mindre, logiska stycken så behöver du använda funktioner. Men hur fungerar detta?
Inbyggda funktioner
Section titled “Inbyggda funktioner”När ett JavaScript-program startar finns ett stort antal inbyggda funktioner tillgängliga direkt. console.log är ett välbekant exempel – den skriver ut ett värde i konsolfönstret. Math.min returnerar det minsta av flera inmatade värden, och Math.random ger tillbaka ett slumpmässigt tal från och med 0 upp till (men inte inklusive) 1. Andra användbara matematikfunktioner är Math.max, Math.floor och Math.ceil.
Varför funktioner?
Section titled “Varför funktioner?”Det finns inte inbyggda funktioner för allt – förr eller senare måste man skriva sina egna. Att dela upp ett program i funktioner är centralt av flera skäl: det strukturerar koden, skapar överblick, och undviker att identisk kod upprepas på flera ställen. Det sistnämnda brukar sammanfattas som principen DRY – Don’t Repeat Yourself – ett begrepp som återkommer genom hela kursen. Funktioner gör dessutom koden enklare att testa.
En bra liknelse är matlagning: “laga mat” är en övergripande funktion som i sin tur innehåller delfunktioner som “koka potatis”, “steka kött” och “göra sås”. På samma sätt bryts ett program ner i lagom stora, namngivna delar.
Anropa en funktion
Section titled “Anropa en funktion”En funktion anropas genom att skriva dess namn följt av parenteser. Inuti parenteserna skickas eventuella argument – de värden som funktionen ska arbeta med:
console.log('Hello, World!') // Skickar en sträng som argumentconsole.log() // Anrop utan argument – skriver ut en tom radlet randomNumber = Math.random() // Returvärdet sparas i en variabelParenteserna är obligatoriska – utan dem sker inget anrop. Varje funktion returnerar alltid ett värde. Saknas en explicit return-sats returneras undefined.
Tre sätt att skapa funktioner
Section titled “Tre sätt att skapa funktioner”Funktionsdeklaration
Section titled “Funktionsdeklaration”Det klassiska sättet: nyckelordet function, följt av ett namn, en parameterlista inom parenteser och en kropp inom klammerparenteser.
function sayHelloTo(name) { let greeting = 'Hello, ' + name + '!' return greeting}
let whatIsSaid = sayHelloTo('Ellen Nu')console.log(whatIsSaid) // OUTPUT: Hello, Ellen Nu!Funktioner skapade med funktionsdeklaration hoistas – JavaScript lyfter dem till toppen av programmet innan koden körs, vilket innebär att de kan anropas även innan de är skrivna i filen.
Funktionsuttryck
Section titled “Funktionsuttryck”Här tilldelas en funktion till en variabel eller konstant. Funktionen behöver inget eget namn.
const anotherSayHelloTo = function (name) { let greeting = 'Hello, ' + name + '!' return greeting}
let whatIsSaid = anotherSayHelloTo('Ellen Nu')console.log(whatIsSaid) // OUTPUT: Hello, Ellen Nu!Använd const för funktionsuttryck – det förhindrar att referensen råkar skrivas över. Till skillnad från funktionsdeklarationer hoistas inte funktionsuttryck, och de kan inte anropas innan de initierats.
Arrow function-syntax
Section titled “Arrow function-syntax”Sedan ES6 (2015) finns ett kortare skrivsätt med en pil (=>):
const yetAnotherSayHelloTo = (name) => { let greeting = 'Hello, ' + name + '!' return greeting}Innehåller kroppen bara en sats kan klammerparenteser och return utelämnas:
const yetAnotherSayHelloTo = (name) => 'Hello, ' + name + '!'Saknar funktionen parametrar måste tomma parenteser ändå anges:
const getTheMeaningOfLife = () => 42Alla tre sätten beter sig likadant när det gäller argument, parametrar och returvärden.
Räckvidd (scope)
Section titled “Räckvidd (scope)”Variabler och parametrar som deklareras inuti en funktion är lokala – de existerar bara inom funktionens kropp och är inte åtkomliga utifrån:
function getFullName(firstName, lastName) { let fullName = firstName + ' ' + lastName // Lokal variabel return fullName}
console.log(fullName) // ReferenceError – fullName finns inte härParametrarna firstName och lastName räknas också som lokala variabler i funktionen.
Parametrar och standardvärden
Section titled “Parametrar och standardvärden”En parameter kan ges ett standardvärde som används om inget argument skickas med vid anropet:
function getFullName(firstName, lastName = 'Doe') { let fullName = firstName + ' ' + lastName return fullName}
getFullName('Ellen', 'Nu') // → 'Ellen Nu'getFullName('Nisse') // → 'Nisse Doe' (standardvärdet används)getFullName() // → 'undefined Doe'Standardvärden på parametrar signalerar till andra utvecklare vilka argument som är obligatoriska och vilka som är valfria. Parametrar utan standardvärde förväntas alltid skickas med.
Moduler
Section titled “Moduler”Jättebra att du kan dela upp din kod i funktioner, men hur gör du om du vill dela upp din kod i flera filer?
Varför moduler?
Section titled “Varför moduler?”När ett program växer och all kod ligger i en enda fil ökar risken för att koden tappar struktur och blir svår att förändra. Genom att dela upp programmets kod i flera filer – moduler – undviks det problemet. En modul är ett litet delprogram som kan ange vilka andra moduler den beror av, och framför allt vilken funktionalitet den erbjuder andra moduler att använda.
ECMAScript-moduler
Section titled “ECMAScript-moduler”Sedan ECMAScript 6 (2015) har JavaScript inbyggt stöd för moduler – ECMAScript-moduler (ES-moduler, ESM). En modul exporterar värden med export:
function bye () { console.log('God bye!') // exporteras inte – bara tillgänglig i modulen}
export function hello () { console.log('Hello, World!') // namngiven export ("named export")}En annan modul laddar den med import:
import * as myGreeting from './my-greeting.js' // namnrymdsimport – importerar allt exporteratimport { hello } from './my-greeting.js' // namngiven import – importerar bara helloBara det som faktiskt exporterats kan anropas utifrån:
myGreeting.hello() // OUTPUT: Hello, World!myGreeting.bye() // ERROR: TypeError: myGreeting.bye is not a functionCommonJS – ett äldre sätt
Section titled “CommonJS – ett äldre sätt”Innan ESM var CommonJS ett av de populära modulsystemen, och används fortfarande mycket inom Node.js eftersom ESM-stödet där länge var experimentellt:
function hello () { console.log('Hello, World!')}
module.exports.hello = hello
// app.jsconst myGreeting = require('./my-greeting.js')myGreeting.hello()Det här sättet räknas idag som utdaterat till förmån för ESM.
Aktivera ECMAScript-moduler i Node.js
Section titled “Aktivera ECMAScript-moduler i Node.js”Node behandlar JavaScript-kod som CommonJS-moduler som standard. För att istället använda ES-moduler läggs fältet type med värdet "module" till i package.json – enklast skapad med npm init -y och sedan kompletterad:
{ "name": "my-awesome-app", "type": "module", "main": "app.js"}Saknas den inställningen ger Node ett tydligt felmeddelande vid körning som talar om att ESM-syntax använts utan att den aktiverats.
npm – en samling paketerad kod
Section titled “npm – en samling paketerad kod”npm-registret innehåller paket, och ett paket kan i sin tur innehålla flera moduler. Vilka paket en applikation använder beskrivs i package.json, under dependencies och devDependencies (tillsammans med vilken version av respektive paket). Paket installeras med npm install PAKETETS_NAMN och hamnar i katalogen node_modules. Saknas den katalogen (till exempel efter en färsk git clone) installeras alla beroenden i ett svep med npm install utan argument.
Kursvecka 3 - Styrstrukturer
Section titled “Kursvecka 3 - Styrstrukturer”Styrstrukturer - Sekvenser
Section titled “Styrstrukturer - Sekvenser”Vad är egentligen en sekvens? Kursledningen repeterar även debuggern i denna inspelning.
Kontrollstrukturer – en överblick
Section titled “Kontrollstrukturer – en överblick”Kontrollstrukturer är grundläggande verktyg vid programmering, och det finns fyra huvudtyper:
- Sekvens – en följd satser som utförs efter varandra.
- Selektion – ett sätt att välja mellan att exekvera olika satser (
if,switch, villkorsoperatorn?:). - Iteration – ett sätt att upprepa en sats ingen, en eller flera gånger (
while,do...while,for). - Rekursion – en funktion som anropar sig själv. Kommer längre fram i kursen.
Sekvens
Section titled “Sekvens”En sekvens är den enklast möjliga kontrollstrukturen: en följd instruktioner som körs i tur och ordning, en efter en.
Instruktion 1Instruktion 2Instruktion 3Instruktion 4Enkel att förstå i isolation – men eftersom en enskild instruktion i sin tur kan innehålla andra kontrollstrukturer (en selektion, en iteration) kan en till synes rak sekvens ändå bli komplicerad att följa i praktiken.
Styrstrukturer - Selektioner
Section titled “Styrstrukturer - Selektioner”Att göra val i koden är en verklig hörnsten i programmeringer. Selektioner är bara ett finare ord för val.
if-satsen
Section titled “if-satsen”Ett enkelt val: är villkorsuttrycket true körs satsen (eller satserna) i if-satsens kropp.
let number = 42if (number === 0) { console.log('Talet är lika med 0.')}Ett villkorsuttryck är ett uttryck som resulterar i något JavaScript betraktar som true eller false. Satserna ska alltid omslutas av klammerparenteser, även när det bara är en enda sats – det gör koden tydligare och mer motståndskraftig mot framtida ändringar.
if...else-satsen
Section titled “if...else-satsen”Ett tvåvägsval: precis ett av alternativen körs, beroende på om villkorsuttrycket är sant eller falskt.
let number = 42if (number % 2 === 0) { console.log('Talet är jämt.')} else { console.log('Talet är udda.')}if...else if...else-satsen
Section titled “if...else if...else-satsen”Ett flervägsval för fler än två alternativ. Villkorsuttrycken testas i tur och ordning, och det första som blir true avgör vilken gren som körs – ett avslutande else (utan eget villkor) kan uteslutas om inget alternativ behöver vara “resten”:
let number = 42if (number < 0) { console.log('Talet är mindre än 0.')} else if (number < 50) { console.log('Talet är större än 0 men mindre än 50.')} else { console.log('Talet är större än, eller lika med, 50.')}// OUTPUT: Talet är större än 0 men mindre än 50.switch-satsen
Section titled “switch-satsen”Ett alternativ till en lång if...else if...else-kedja när flera värden ska jämföras mot samma variabel. case-grenar utan egen kropp “faller igenom” till nästa case som har en – praktiskt när flera värden ska hanteras likadant:
let number = 4switch (number) { case 1: case 2: case 3: case 4: case 5: case 6: console.log('För litet!') break case 7: console.log('Rätt gissat') break case 8: case 9: case 10: console.log('För stort!') break default: console.log(number + ' är inte i det slutna intervallet mellan 1 och 10.') break}// OUTPUT: För litet!break avslutar switch-satsen – glöms det bort fortsätter exekveringen rakt in i nästa case, oavsett dess eget villkor.
Villkorsoperatorn (?:)
Section titled “Villkorsoperatorn (?:)”En vanlig if...else-sats kan ibland lämpligen uttryckas mer kompakt med villkorsoperatorn – den enda operatorn i JavaScript som tar tre operander:
const number = 42let output = 'Talet är 'output += number % 2 === 0 ? 'jämt.' : 'udda.'console.log(output)// OUTPUT: Talet är jämt.Motsvarar exakt samma logik som en fullständig if...else-sats, men som ett uttryck snarare än en sats – lämpar sig bäst för korta, tydliga val, inte för mer invecklad förgrening.
Styrstrukturer - Iterationer
Section titled “Styrstrukturer - Iterationer”Inte sällan behöver samma sak göras flera gånger, alltså upprepas. Det händer alltså att du i din kod behöver upprepa dig för att vara extra tydlig!
while-satsen – förtestad upprepning
Section titled “while-satsen – förtestad upprepning”Så länge villkorsuttrycket utvärderas till true körs satserna i loopens kropp. Villkoret testas innan varje varv, så loopen kan köras noll, en eller flera gånger:
let sum = 0while (sum < 500) { sum = sum + 42}console.log(sum) // OUTPUT: 504do...while-satsen – eftertestad upprepning
Section titled “do...while-satsen – eftertestad upprepning”Satserna i loopens kropp körs minst en gång, eftersom villkoret testas efter varje varv:
let i = 10do { console.log(i + ' ') i++} while (i < 10)// OUTPUT: 10 – körs en gång trots att villkoret redan var falsktfor-satsen – bestämd upprepning
Section titled “for-satsen – bestämd upprepning”Ett annat sätt att uttrycka en while-sats, med initiering av räknare, villkorsuttryck och uppdatering av räknaren samlat på en rad:
let output = ''for (let i = 0; i < 10; i++) { output += i + ' '}console.log(output) // OUTPUT: 0 1 2 3 4 5 6 7 8 9Ordningen är: (1) initiera räknaren en gång, (2) testa villkorsuttrycket, (3) kör satserna, (4) uppdatera räknaren och gå tillbaka till steg 2.
Avbryta en loop – break
Section titled “Avbryta en loop – break”break-satsen avbryter en loop omedelbart, oavsett loopens eget villkor:
let output = ''for (let i = 0; i < 10; i++) { if (i === 4) { break } output += i + ' '}console.log(output) // OUTPUT: 0 1 2 3Gå till nästa iteration – continue
Section titled “Gå till nästa iteration – continue”continue-satsen avslutar den aktuella iterationen och hoppar direkt till nästa varv, utan att köra resten av loopens kropp den gången:
let output = ''for (let i = 0; i < 10; i++) { if (i % 3 === 0) { continue } output += `${i} `}console.log(output) // OUTPUT: 1 2 4 5 7 8continue anses av vissa vara “bad practice” – koden kan i regel omformuleras (till exempel med ett inverterat villkor) så att continue inte behövs.
Kommentarer
Section titled “Kommentarer”Att kommentera din kod på ett strukturerat sätt är viktigt. Kursledningen ger en inledning till detta här. Missa inte guiden kring JSDoc-kommentarer.
Vad gör koden?
Section titled “Vad gör koden?”Odokumenterad kod kräver att man läser och förstår den i detalj för att inse vad den gör:
function m (a, b) { if (a === 0 || b === 0) { return 0 } if (b < 0) { return -a + m(a, b + 1) } return a + m(a, b - 1)}Ett otydligt namn (m), inga uppgifter om vad parametrarna eller returvärdet betyder, och en funktion som anropar sig själv (rekursion) utan förklaring till varför – allt det gör koden svårare att förstå än den behöver vara.
Vad gör den dokumenterade koden?
Section titled “Vad gör den dokumenterade koden?”Samma funktion, men med ett tydligare namn och en JSDoc-kommentar:
/** * Multiplies two numbers by repeated addition and returning the product. * * @param {number} a - The multiplicand. * @param {number} b - The multiplier. * @returns {number} The product of the two numbers. */function multiply (a, b) { // Base case if (a === 0 || b === 0) { return 0 } // Negative multiplier if (b < 0) { return -a + multiply(a, b + 1) } // Positive multiplier return a + multiply(a, b - 1)}Som bonus använder editorer som VS Code JSDoc-kommentaren för att visa information om funktionen direkt vid anrop, och den kan även användas för att generera automatisk HTML-dokumentation.
Vad är kommentarer?
Section titled “Vad är kommentarer?”Kommentarer används för att förklara kod och göra ett program mer läsbart – även för den som skrev koden från början. De kan också användas för att temporärt hindra kod från att exekveras, vilket är användbart vid felsökning. Det finns tre typer: dokumentationskommentarer (JSDoc), radkommentarer och blockkommentarer.
Dokumentationskommentarer med JSDoc
Section titled “Dokumentationskommentarer med JSDoc”JSDoc är ett märkspråk för att dokumentera moduler, funktioner (parametrar och returvärde), klasser och deras medlemmar. En JSDoc-kommentar inleds med /** och avslutas med */:
/** * Outputs a greeting. * * @param {string} name - The name of the person to greet. */function greet (name) { console.log('Hi, ' + name + '!')}Den enklaste formen är bara en beskrivning utan taggar; JSDoc-taggar som @param och @returns ger mer strukturerad information.
Radkommentarer
Section titled “Radkommentarer”Radkommentarer, som börjar med //, kommenterar resten av raden. De kan stå på en egen rad eller i slutet av en kodrad (en “inline”-kommentar):
// The answer to the ultimate question of life.let meaningOfLife = 42
// Output the variable's value to the console.console.log(meaningOfLife)Det finns delade meningar om inline-kommentarer – en känd synpunkt (Jeff Raskin) är att en kommentar förtjänar minst en egen rad, hellre än att klämmas in efter kod. Lämpligast står en radkommentar på en egen rad, med en tom rad ovanför, om den inte används för att tillfälligt kommentera bort kod.
Blockkommentarer
Section titled “Blockkommentarer”Blockkommentarer, som börjar med /* och slutar med */, kommenterar bort allt däremellan – en del av en rad, eller flera hela rader:
/* This isa multi-line comment.Only code outside of thismulti-line comment will be executed.*/console.log('Here is a /*cat*/ 🐈!')Flera på varandra följande radkommentarer används ofta för samma syfte – att tillfälligt kommentera bort ett kodblock – som ett alternativ till en enda blockkommentar.
Kursvecka 4 - Arrayer
Section titled “Kursvecka 4 - Arrayer”Exempel - Vilket Unicode-värde
Section titled “Exempel - Vilket Unicode-värde”Kursledningen inleder till viss del med repetition för att förstärka delar som rör funktioner och moduler.
Exempelkoden hittar du på Gitlab.
Utgångsläget: allt i en sekvens
Section titled “Utgångsläget: allt i en sekvens”Exemplet startar som ett litet, fungerande program: det läser in en textrad och ett index från användaren (via paketet readline-sync) och skriver ut vilket tecken och Unicode-värde som finns på det indexet, tills ett negativt index avslutar programmet. Hela logiken ligger som en enda lång sekvens av satser i app.js – precis det som gör koden svår att testa och underhålla när ett program växer.
Ett första steg mot bättre struktur: identifiera vilka satser som hör ihop, och bryt ut dem till egna funktioner.
Steg 1: bryt ut satser till funktioner
Section titled “Steg 1: bryt ut satser till funktioner”Två tydliga kandidater identifieras: utskriften av hjälptexten och läsningen av ett index. De blir varsin funktion:
const printInstructions = function () { console.log('...')}
const getIndexFromUser = function () { return Number.parseInt(readlineSync.question('Enter an index (negative value to quit): '))}getIndexFromUser används på två ställen i originalkoden (en gång innan loopen, en gång sist i loopens kropp) – att bryta ut den till en funktion tar bort just den upprepningen. En funktion som getIndexFromUser behöver inga parametrar eftersom den bara returnerar ett värde den själv hämtar in.
Steg 2: samla allt i en main-funktion
Section titled “Steg 2: samla allt i en main-funktion”Med flera funktioner definierade blir det otydligt var exekveringen egentligen börjar. Lösningen är att samla den kvarvarande “lösa” koden i en egen funktion, av tradition ofta kallad main (ett arv från C-liknande språk):
const main = function () { // all kvarvarande kod, inklusive while-loopen}
main()Anropet av main() sist i filen blir den enda platsen exekveringen faktiskt startar – tydligare än att låta lös kod ligga utspridd bland funktionsdeklarationerna.
Steg 3: bryt ut funktionerna till en egen modul
Section titled “Steg 3: bryt ut funktionerna till en egen modul”Fortfarande ligger alla funktioner i samma fil som main. Nästa steg i omstruktureringen är att gruppera de funktioner som hör ihop och flytta dem till en egen modul – samma princip som att gruppera satser i funktioner, bara ett steg upp. En ny fil (namngiven i kebab-case, i linje med Node-modulers namnkonvention) samlar hjälpfunktionerna, och exporterar dem med namngivna exports:
export const printInstructions = function () { ... }export const getIndexFromUser = function () { ... }// osv.import * as UnicodePicker from './unicode-picker.js'
const main = function () { UnicodePicker.printInstructions() // ...}Ett par detaljer som gav verkliga felmeddelanden på vägen: importsökvägen till en egen ES-modul måste innehålla filändelsen (.js) – till skillnad från CommonJS, där det brukar fungera utan. Och en modul utan export default kan inte importeras med ett vanligt import Namn from ... – då krävs antingen namnrymdsimport (import * as Namn from ...) för att få tag i alla namngivna exports samlade under ett gemensamt namn, eller en specifik namngiven import (import { specifikFunktion } from ...) om bara någon enstaka funktion behövs. export default avråds ifrån här – att alltid skriva ut exakt vad som exporteras gör koden tydligare.
Slutresultatet: app.js innehåller bara main, som läses nästan som en innehållsförteckning över vad programmet gör; all den specifika logiken ligger samlad i en egen, namngiven modul.
Lärdomar
Section titled “Lärdomar”Att lägga ett så här litet exempel (ett tjugotal rader) i flera filer och moduler kan kännas onödigt omständigt för just det här problemet – poängen är träningen i sig, eftersom samma disciplin blir avgörande först när ett program vuxit till tusentals rader. Det är för sent att lära sig strukturera kod först då. Detsamma gäller dokumentation: att skriva en funktions JSDoc-kommentar innan man skriver dess innehåll tvingar fram klarhet i vad funktionen faktiskt ska göra, istället för att dokumentationen blir en eftertanke som ofta uteblir helt i tidspress.
Arrayer
Section titled “Arrayer”Att kunna organisera större dataset i listor, arrayer, är mycket kraftfullt. En lång föreläsning på ämnet ligger till grund för dessa delar.
Varför arrayer?
Section titled “Varför arrayer?”En variabel kan bara hålla ett värde, och en funktion kan bara returnera ett värde. Men mycket av det vi vill programmera handlar inte om enstaka värden utan om samlingar av värden – en lista med vänner, en kundvagn, rader i en handlingslista. Lösningen är en referens till en array eller ett objekt, där själva samlingen kan innehålla hur många värden som helst.
En array är en ordnad lista med värden. Ordnad betyder här att raderna behåller sin inbördes ordning – inte att värdena är sorterade.
Skapa en array
Section titled “Skapa en array”Det rekommenderade sättet är en array-literal med hakparenteser:
const groceries = [ 'mjölk', 'ägg', 'pasta', 'ketchup', 'nudlar', 'havregryn']Varje värde i listan kallas ett element, och elementen numreras från och med 0 – ett 0-baserat index. groceries[4] ger alltså det femte elementet, 'nudlar', inte det fjärde.
Det går också att skapa en array med new Array(...), men det avråds ifrån – konstruktorn beter sig olika beroende på hur många argument som skickas in. new Array(28, 18, 26) skapar en array med de tre talen, men new Array(10) skapar inte en array med värdet 10, utan en array med tio tomma (undefined) element. Array-literalen har inte den fällan.
Alla värden i en array bör vara av samma datatyp. JavaScript tillåter att man blandar typer ([42, 'är meningen med livet', true]), men det är en dålig idé – blir det en samling av olika typer av data hör den hemma i ett objekt istället.
Arbeta med element
Section titled “Arbeta med element”Index används både för att läsa och skriva ett elements värde:
const temperatures = [12.6, 18.7, 9.8, 13.2, 14.4]
console.log(temperatures[3]) // OUTPUT: 13.2temperatures[1] = 4.2 // tilldelar det andra elementet ett nytt värde
console.log(temperatures.length) // OUTPUT: 5length är en egenskap, inte en metod – inga parenteser.
Iterera med for-loop
Section titled “Iterera med for-loop”Att en array har ett 0-baserat index och en känd längd är precis varför for-loopen ofta ser ut som den gör:
let output = ''for (let i = 0; i < temperatures.length; i++) { output += temperatures[i] + ' '}Loopvillkoret ska vara i < length, inte i <= length – längden 5 betyder att sista giltiga indexet är 4. Försöker man läsa ett index utanför arrayen kraschar inte JavaScript (till skillnad från många andra språk) – man får undefined tillbaka istället.
Lägga till och ta bort element
Section titled “Lägga till och ta bort element”De vanligaste metoderna för att ändra en arrays storlek:
push(värde)– lägger till sistpop()– tar bort och returnerar det sista elementetunshift(värde)– lägger till förstshift()– tar bort och returnerar det första elementet
splice() kan lägga till, ta bort eller ersätta element mitt i en array, men används mer sällan.
Värdetyper och referenstyper
Section titled “Värdetyper och referenstyper”Det här är den mest centrala – och klurigaste – delen. Number (och de andra primitiva typerna) är en värdetyp: när en variabel tilldelas en annan variabels värde kopieras själva värdet, och de två variablerna blir sedan helt oberoende av varandra.
let dogAge = 7let catAge
catAge = dogAge // 7 kopieras till catAgecatAge = 3
console.log(catAge) // OUTPUT: 3console.log(dogAge) // OUTPUT: 7 – opåverkadEn array är däremot ett objekt, och objekt är referenstyper. Tilldelningen kopierar inte själva arrayen, bara en referens till den – som att ge ut ett extra koppel till samma hund. Två variabler kan referera till en och samma array:
const numbers = [5, 8, 7, 1, 6]let figures
figures = numbers // figures får en referens till SAMMA arrayfigures[0] = 42
console.log(figures) // OUTPUT: [ 42, 8, 7, 1, 6 ]console.log(numbers) // OUTPUT: [ 42, 8, 7, 1, 6 ] – båda påverkas!Anledningen till att referenser fungerar så här är prestanda: en array kan innehålla hundratusentals värden (tänk en bildfil, pixel för pixel), och att kopiera hela innehållet varje gång skulle vara orimligt dyrt. Att kopiera en referens – i praktiken en minnesadress – kostar i princip ingenting.
Referenser som argument och returvärde
Section titled “Referenser som argument och returvärde”Samma resonemang gäller när en array skickas till eller returneras från en funktion. Skickas en array in som argument arbetar funktionen på samma array som anroparen har, vilket kan ge oönskade sidoeffekter:
const logDoubledUp = function (results) { for (let i = 0; i < results.length; i++) { results[i] *= 2 } console.log(results)}
const scores = [18, 9, 23, 12, 21]logDoubledUp(scores)console.log(scores) // OUTPUT: [ 36, 18, 46, 24, 42 ] – scores har muterats!Vill man undvika det arbetar man på en kopia istället, till exempel med Array.from(), spridningsoperatorn [...array] eller (äldre) .slice():
const logDoubledUp = function (results) { const copy = Array.from(results) for (let i = 0; i < copy.length; i++) { copy[i] *= 2 } console.log(copy)}Sortera en array
Section titled “Sortera en array”sort() utan argument konverterar först elementen till strängar och jämför dem lexikografiskt – vilket ger överraskande resultat för tal (10 hamnar före 2). Ska talen sorteras numeriskt behövs en egen jämförelsefunktion:
const values = [2, 42, 10, 1, 8, 24, 4]values.sort(function (a, b) { return a - b})// OUTPUT: [ 1, 2, 4, 8, 10, 24, 42 ]Fler metoder: forEach, reduce och map
Section titled “Fler metoder: forEach, reduce och map”forEach itererar igenom värdena, reduce reducerar dem till ett enda värde (till exempel en summa), och map skapar en helt ny array genom att transformera varje värde:
const values = [2, 1, 8]
values.forEach(function (value) { console.log(value)})
const sum = values.reduce(function (a, b) { return a + b}, 0) // OUTPUT: 11
const doubledValues = values.map(value => value * 2) // OUTPUT: [ 4, 2, 16 ]Både forEach och reduce tar emot en funktion som argument, vilket gör dem till exempel på det som kallas “higher-order functions”. map-exemplet visar också en arrow function – ett kortare sätt att skriva ett funktionsuttryck på, där function och return kan uteslutas när kroppen bara innehåller en sats.
Undantag
Section titled “Undantag”Redan nu smygs en kort introduktion till undantag in.
Något kommer att gå fel
Section titled “Något kommer att gå fel”Även om koden är korrekt utifrån vad man känner till kan en applikation stöta på fel: felaktig indata, ett API som svarar annorlunda än väntat, eller helt enkelt ett misstag i koden. Det här blir extra tydligt så fort en applikation tar emot data utifrån – från en användare, ett nätverksanrop eller en extern tjänst – för då vet man aldrig med säkerhet vad som faktiskt kommer in.
const name = 'Lovelace'name = 'Hopper' // TypeError: Assignment to constant variable.console.log(name) // körs aldrigNär ett exekveringsfel inträffar kastas ett undantag, och exekveringen av programmet avbryts omedelbart där felet uppstod – satsen efter felet körs aldrig.
Vad är ett undantag?
Section titled “Vad är ett undantag?”Ett undantag är inget annat än ett objekt som representerar felet. JavaScript har flera inbyggda feltyper som används vid olika sorters fel: Error, EvalError, InternalError, RangeError, ReferenceError, SyntaxError, TypeError och URIError. I egen kod är det oftast Error och TypeError man kastar.
Kasta undantag
Section titled “Kasta undantag”Utöver att JavaScript själv kastar undantag kan man som programmerare aktivt kasta ett eget, med det reserverade ordet throw. Ett felobjekt skapas med new och tar oftast ett meddelande som argument:
const getDigitSum = function (value) { if (typeof value !== 'number') { throw new TypeError('The argument passed must be a Number.') } // summera siffrorna och returnera summan}Poängen med att kontrollera indata och kasta ett tydligt fel direkt är dels att undvika konstiga följdfel längre in i koden, dels att en funktion i praktiken kan anropas med vilket argument som helst – det finns ingen garanti för att den som anropar skickar in rätt typ.
Felobjektet har egenskaperna name (vilken feltyp) och message (det egna meddelandet), på samma sätt som vilket annat objekt som helst.
Fånga undantag
Section titled “Fånga undantag”Kod som riskerar att kasta ett undantag placeras i ett try-block. Kastas ett undantag där hoppar exekveringen direkt till catch-blocket, som får tillgång till felobjektet och kan hantera det – till exempel logga ett mer användarvänligt meddelande istället för att låta hela applikationen krascha med en teknisk stacktrace. Ett eventuellt finally-block körs alltid, oavsett om ett fel kastades eller inte.
try { const result = getDigitSum('This is not a number') console.log(`The sum of the digits is ${result}.`)} catch (err) { console.error(err.message)} finally { // körs alltid}try/catch/finally kan kombineras som try-catch, try-finally eller alla tre tillsammans. Blir det inget fel i try-blocket exekveras inte catch-blocket – men finally körs ändå.
Så länge koden själv har full kontroll (till exempel att bara skicka in tal man redan vet är tal) behövs inget try/catch. Det blir nödvändigt först när koden gör saker den inte kan garantera lyckas – och det tydligaste exemplet är nätverksanrop över internet, där en kabel kan vara av eller en server nere. Det är ett ämne som återkommer längre fram i kursen.
Kursvecka 5 - Objekt
Section titled “Kursvecka 5 - Objekt”Objekt
Section titled “Objekt”Objekt är något oerhört centralt i JavaScript och i all objektbaserad programmering. Denna inspelning introducerar objekt ur dess allra renaste form.
Skapa ett objekt
Section titled “Skapa ett objekt”Ett objekt skapas med två klammerparenteser, precis som en array skapas med två hakparenteser:
const lovelace = {}Även ett “tomt” objekt är i praktiken inte helt tomt. I bakgrunden finns alltid ett grundobjekt, Object, som varje objekt i JavaScript i grunden bygger vidare på – ungefär som en tom array redan har egenskaper som length och metoder som push. Det går att skapa ett objekt med new Object() också, men {} – en objektlitteral – är det moderna och rekommenderade sättet.
Ett objekt är ett sätt att paketera data. Till skillnad från en array – som ska hålla samma datatyp i varje element – kan ett objekt fritt blanda tal, strängar, booleaner och referenser till andra objekt.
Egenskaper och prototypkedjan
Section titled “Egenskaper och prototypkedjan”Det som finns i objektet kallas egenskaper (properties), och varje egenskap är i grunden ett nyckel-värde-par: en nyckel (namnet på egenskapen) kopplad till ett värde. Utöver de egenskaper man själv lägger till har varje objekt en intern egenskap (skriven __proto__) som pekar mot det bakomliggande grundobjektet – prototypen. Interna egenskaper av det slaget, inramade med dubbla understreck, ska man varken läsa eller ändra direkt; JavaScript sköter dem själv.
Anropas en metod som inte finns direkt på objektet letar JavaScript vidare uppåt i prototypkedjan. Finns metoden inte där heller blir det ett fel. Det här förklarar varför lovelace.toString() fungerar på ett objekt som aldrig själv fått en toString-egenskap – den hittas i stället på prototypen.
Sätta och hämta en egenskap
Section titled “Sätta och hämta en egenskap”Egenskaper sätts och hämtas med punktnotation:
lovelace.firstName = 'Ada'lovelace.lastName = 'Lovelace'
console.log(lovelace.firstName) // OUTPUT: AdaFinns inte egenskapen redan skapas den; finns den redan skrivs värdet över. JavaScript är här mycket tillåtande – stavar man fel skapas helt sonika en ny egenskap istället för att ett fel uppstår, vilket kan vara nog så lurigt att felsöka.
Det går även att komma åt egenskaper med hakparentesnotation (lovelace['firstName']), men kursens kodstandard kräver punktnotation eftersom det är tydligare – hakparenteser är egentligen till för fall där namnet på egenskapen i sig är dynamiskt (lagrat i en variabel), inte hårdkodat.
Egenskaper kan också sättas direkt när objektet skapas, med kolon istället för likhetstecken och kommatecken mellan varje par:
const lovelace = { firstName: 'Ada', lastName: 'Lovelace'}Ta bort en egenskap
Section titled “Ta bort en egenskap”Nyckelordet delete tar bort en egenskap helt:
delete lovelace.birthYearAtt istället sätta värdet till undefined är inte samma sak – då finns egenskapen kvar, bara med ett tomt värde.
Metoder – en egenskap vars värde är en funktion
Section titled “Metoder – en egenskap vars värde är en funktion”En egenskap vars värde är en funktion kallas en metod. Det finns flera sätt att skriva en, men det kortare, nyare sättet (sedan ES6/2015) är att utelämna både kolon och function:
const lovelace = { firstName: 'Ada', lastName: 'Lovelace', says () { return `Hi, I'm ${this.firstName}` }}
console.log(lovelace.says()) // OUTPUT: Hi, I'm AdaNotera return – låter man metoden console.log direkt istället för att returnera strängen bryter man mot separation of concerns: metoden bör lämna ifrån sig ett värde, inte själv bestämma hur det skrivs ut.
Nyckelordet this refererar till det objekt som “kör koden” – mer precist: det objekt en metod anropas för. Anropas lovelace.says() sätts this att referera till lovelace under körningen av says. Fördelen är att metoden blir generell – den bryr sig inte om objektet råkar heta lovelace eller något helt annat, och kan därmed återanvändas rakt av på flera olika objekt istället för att hårdkoda ett specifikt variabelnamn inuti sig själv.
Undvika upprepning av kod
Section titled “Undvika upprepning av kod”Skapar man flera objekt av samma “form” – till exempel lovelace, hopper och hamilton, alla med firstName, lastName och says – hamnar samma metod dubbeldeklarerad i varje objekt. Det är upprepning av kod, precis det man vill undvika. En tanke kan vara att lyfta upp en delad metod till det gemensamma grundobjektet (Object.prototype) istället, men det är i praktiken en dålig idé – det objektet delas av alla objekt i hela programmet (och i en webbläsarmiljö av alla komponenter på sidan, oavsett vem som skapat dem), så att bygga på det riskerar att krocka med annan kod. Ett bättre svar på problemet – klasser – kommer i nästa vecka.
Kursvecka 6 - Klasser
Section titled “Kursvecka 6 - Klasser”Exempel - Ordräknaren
Section titled “Exempel - Ordräknaren”Ett moment som många har svårt med i examinationsuppgift A2 är funktionen “Mode”. För att underlätta löses här ett exempel som löser samma grundläggande problem.
Exempelkoden hittar du på Gitlab.
Uppgiften
Section titled “Uppgiften”Ett förberett dataunderlag – en array med ett antal korta citat, sammanfogade till en enda lång text med join('\n') – ska analyseras: hur många gånger förekommer varje ord, och vilket eller vilka ord är vanligast? Övningen är tänkt som en mindre variant av samma problem som löses i examinationsuppgift A2 (mode), utan att vara identisk med den.
Innan kodandet börjar är det värt att lösa problemet på papper: skriva upp varje ord man stöter på, kontrollera om det redan finns uppskrivet, och antingen lägga till det eller öka en räknare. Det tankesättet – ett ords namn kopplat till ett antal – är exakt vad ett objekt (nyckel-värde-par) är till för.
Städa texten
Section titled “Städa texten”Rå text innehåller skiljetecken och radbrytningar som stör en enkel uppdelning i ord. Ett första försök tar bort punkter och kommatecken med String#replace/replaceAll (replaceAll fungerar bara i tillräckligt nya Node-versioner, vilket upptäcktes live via ett felmeddelande – ett bra exempel på att kontrollera dokumentationen och sin egen miljö innan man antar att en metod finns tillgänglig). En sträng är immutable – replace returnerar alltid en ny sträng, den ursprungliga strängen kan aldrig ändras i sig.
Efter en första körning dyker fler problem upp i utskriften: kvarvarande radbrytningstecken (\n) mellan citaten. Att byta ut radbrytningarna mot mellanslag (inte mot en tom sträng) blir den sista pusselbiten – annars klistras ord från olika citat ihop till ett enda felaktigt ord.
Dela upp i ord
Section titled “Dela upp i ord”String#split(' ') delar upp den rensade, gemenifierade (toLowerCase()) textmassan till en array av ord. (En alternativ metod, slice, undersöktes också men visade sig inte passa – den extraherar en delsträng utifrån position, inte utifrån ett sökmönster.)
Bygg en frekvenstabell
Section titled “Bygg en frekvenstabell”Ett tomt objekt skapas utanför loopen (skapas det inuti loopen istället byggs ett nytt, tomt objekt upp på varje varv – ett vanligt nybörjarmisstag). Sedan itereras ordlistan igenom med en for...of-loop, som är att föredra framför en klassisk indexbaserad for-loop när själva indexet inte behövs:
const wordFrequency = {}
for (const word of words) { if (!wordFrequency[word]) { wordFrequency[word] = 1 } else { wordFrequency[word]++ }}Att fråga efter en egenskap som inte finns på ett objekt (wordFrequency[word]) ger undefined, vilket är falsy – precis det villkoret bygger på för att avgöra om ordet redan setts eller inte.
Hitta det mest förekommande ordet
Section titled “Hitta det mest förekommande ordet”Att bara plocka ut det numeriskt största värdet räcker inte – uppgiften efterfrågar vilket/vilka ord som är vanligast, inte hur många gånger. Object.values() ger alla frekvenser som en array (för att till exempel sortera fram det högsta värdet); Object.keys() ger motsvarande lista med alla unika ord. Med maxfrekvensen känd kan Object.keys(wordFrequency) itereras igenom en gång till, och varje ord vars frekvens matchar maxvärdet samlas i en resultatarray:
const result = []for (const key of Object.keys(wordFrequency)) { if (wordFrequency[key] === maxFrequency) { result.push(key) }}En detalj värd att lägga märke till: Object.keys(obj) inuti själva for...of-uttrycket utvärderas bara en enda gång totalt, inte en gång per iteration – att spara resultatet i en egen namngiven variabel är ändå ofta tydligare att läsa.
Från utforskande kod till färdig lösning
Section titled “Från utforskande kod till färdig lösning”Koden ovan byggdes utforskande, steg för steg, med tillfälliga variabelnamn och lite extra krångel på vägen – helt normalt när man löser ett nytt problem, och något att städa upp i efterhand snarare än att sträva efter en perfekt lösning direkt. Den färdiga referenslösningen bryter ut logiken till en separat modul (text-analyzer.js) med tre rena, exporterade funktioner – en för att dela upp text i ord, en för att räkna ordfrekvens, och en för att hitta de mest förekommande orden – och finns i två varianter: en med samma traditionella teknik som visas här, och en som använder nyare, mer kompakta ES6-konstruktioner (itererande array-metoder) som kursen återkommer till senare.
Klasser
Section titled “Klasser”I JavaScript kan objekt konstrueras på många olika sätt.
Allt handlar om prototypkedjor
Section titled “Allt handlar om prototypkedjor”Innan klass-syntax fanns i JavaScript byggdes prototypkedjor upp för hand, ofta med hjälp av det som kallas “factory”-designmönstret – en funktion som skapar och returnerar ett objekt:
const pioneerPrototype = { says (text) { return `${this.firstName} says "${text}"` }}
const createPioneer = function (firstName, lastName) { const pioneer = Object.create(pioneerPrototype) pioneer.firstName = firstName pioneer.lastName = lastName return pioneer}
const lovelace = createPioneer('Ada', 'Lovelace')Object.create(pioneerPrototype) skapar ett nytt objekt vars interna __proto__ pekar på pioneerPrototype. Nästan alla objekt i JavaScript har en sådan kedja av ett eller flera prototypobjekt – det är språkets viktigaste sätt att återanvända funktionalitet mellan objekt.
Klassdeklaration
Section titled “Klassdeklaration”Klass-syntax är ett kompakt sätt att sätta upp exakt samma sorts prototypkedja:
class Pioneer { constructor (firstName, lastName) { this.firstName = firstName this.lastName = lastName }
says (text) { return `${this.firstName} says "${text}"` }}
const lovelace = new Pioneer('Ada', 'Lovelace')En klass är i grunden bara en funktion – typeof Pioneer ger 'function'. Precis som för alla funktioner skapas automatiskt egenskapen prototype, vars värde är ett objekt som i sin tur innehåller egenskapen constructor. says-metoden hamnar på Pioneer.prototype, inte direkt på varje instans – alla instanser delar alltså samma metodobjekt.
Varför klasser och inte “constructor/prototype”?
Section titled “Varför klasser och inte “constructor/prototype”?”Innan klass-syntax fanns användes vanliga konstruktorfunktioner tillsammans med manuell .prototype-tilldelning för att uppnå samma sak. Klasser föredras idag av flera skäl: de används genomgående när man skriver webbkomponenter, ramverk som React och Angular bygger på dem, och JavaScript-motorer är bra på att optimera kod skriven med klass-syntax. Arv är också betydligt enklare att implementera med extends, även från inbyggda konstruktorfunktioner.
Det finns baksidor att vara medveten om: skillnaden mellan hur klasskod ser ut och vad som faktiskt sker under huven är stor, det är lockande att stoppa för mycket funktionalitet i en och samma klass, och arv är så enkelt att tillämpa att det ibland görs i för stor omfattning.
Flera sätt att definiera en klass
Section titled “Flera sätt att definiera en klass”Utöver klassdeklaration (class Pioneer { ... }) finns klassuttryck, både anonyma och namngivna:
// Anonymt klassuttryckconst Pioneer = class { ... }
// Namngivet klassuttryckconst Pioneer = class MyClass { ... }Oavsett hur klassen definierats skapas en instans med new följt av klassnamnet och eventuella argument.
Klassdefinitionens delar
Section titled “Klassdefinitionens delar”Alla delar av en klassdeklaration ger motsvarande egenskap på Pioneer.prototype – förutom de som föregås av static, som istället hamnar direkt på klassen (funktionen) själv:
class Pioneer { constructor (firstName, lastName) { this.firstName = firstName this.lastName = lastName }
static from (other) { return new Pioneer(other.firstName, other.lastName) }
get fullName () { return `${this.firstName} ${this.lastName}` }
says (text) { return `${this.firstName} says "${text}"` }}
const lovelace = new Pioneer('Ada', 'Lovelace')const copy = Pioneer.from(lovelace)constructor anropas automatiskt när en ny instans skapas och kan uteslutas helt om ingen initiering behövs. fullName är en getter – en metod som beter sig som en vanlig (skrivskyddad) egenskap när den läses, utan parenteser vid anropet: lovelace.fullName, inte lovelace.fullName(). En static-metod som from anropas direkt på klassen (Pioneer.from(...)), inte på en instans, och används ofta som en alternativ “fabrik” för att skapa objekt – här en kopia baserad på ett annat objekts egenskaper.
Kapsla in kod med klasser
Section titled “Kapsla in kod med klasser”Ett enkelt exempel: en tärning. Ett första försök håller ett värde och en metod som slumpar fram ett nytt:
class Die { constructor (faceValue) { this.faceValue = faceValue }
roll () { this.faceValue = Math.floor(Math.random() * 6) + 1 }}
const die = new Die()Problemet: konstruktorn initierar inte objektet på ett bra sätt – faceValue sätts inte till ett giltigt slumpat värde direkt, och antalet sidor är hårdkodat till sex. Ett andra försök löser båda: konstruktorn anropar roll direkt, och antalet sidor blir en parameter med ett standardvärde:
class Die { constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }
roll () { this.faceValue = Math.floor(Math.random() * this.maxFaceValue) + 1 }}
const die = new Die()console.log(die.faceValue) // ett tal mellan 1 och 6Skickas inget argument med skapas en sexsidig tärning; anges ett annat värde (t.ex. new Die(20)) skapas en tärning med det antalet sidor istället. Ett kvarstående problem: faceValue och maxFaceValue går fortfarande att sätta till vilket värde som helst utifrån, eftersom inget skyddar de “riktiga” egenskaperna – någonting kommande presentationer tar upp.
Kursvecka 7 - Arv
Section titled “Kursvecka 7 - Arv”Inkapsling
Section titled “Inkapsling”Ska alla egenskaper på ett instansierat objekt alltid vara tillgängliga utanför klassen?
Problemet: oskyddade egenskaper
Section titled “Problemet: oskyddade egenskaper”En tärningsklass med två egenskaper (faceValue, maxFaceValue) fungerar bra i normalfallet, men inget hindrar att egenskaperna sätts till vilket värde som helst utifrån:
export class Die { constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }
roll () { this.faceValue = Math.floor(Math.random() * this.maxFaceValue) + 1 }}
const die = new Die()die.faceValue = 'This is a strange value for a die!' // fullt tillåtet, men feldie.maxFaceValue = 'twentyish'die.roll() // resultatet blir NaNGår det att dölja eller skydda en del av ett objekts data för omvärlden? Ja.
Ett fält är egentligen bara en egenskap, men deklarerad ovanför konstruktorn i klassdefinitionen istället för att skapas dynamiskt inuti den. Fördelen är att alla en klass egenskaper syns samlade på ett ställe:
class Die { maxFaceValue // ett fält
constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }}Ett fält kan vara publikt eller privat – att det är ett fält handlar bara om var egenskapen deklareras, inte om den är skyddad.
Semiprivata egenskaper (den gamla konventionen)
Section titled “Semiprivata egenskaper (den gamla konventionen)”Innan JavaScript hade riktiga privata fält användes en namnkonvention: ett inledande understreck (_age) signalerade “rör inte den här utifrån” – men skyddade den inte på något sätt rent tekniskt. En sådan semiprivat egenskap går fortfarande att läsa och skriva till precis som vilken annan egenskap som helst. Konventionen lever kvar i mycket äldre kod, men är på väg att fasas ut till förmån för riktiga privata fält.
Privata fält
Section titled “Privata fält”Ett privat fält skapas med ett inledande nummertecken (#) och kan bara nås inifrån klassdefinitionen – ett genuint språkskydd, inte bara en konvention:
class Die { #faceValue #maxFaceValue
constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }}Ett privat fält måste deklareras (skrivas ut som fält i klassdefinitionen) innan det kan användas – till skillnad från vanliga egenskaper kan de inte skapas dynamiskt “i farten”. Försöker kod utanför klassen komma åt ett privat fält blir det ett tydligt fel direkt, istället för att bara ge undefined.
Getters och setters
Section titled “Getters och setters”Ett privat fält är helt otillgängligt utifrån om inget annat görs – även för att bara läsa av det. Lösningen är accessor- och mutatormetoder: en getter (get) binder en egenskap till en funktion som anropas när värdet hämtas, en setter (set) binder egenskapen till en funktion som anropas när värdet sätts:
class Dog { #age
constructor (name, age) { this.name = name this.age = age // anropar settern }
get age () { return this.#age }
set age (value) { if (!Number.isInteger(value) || value < 1) { throw new Error('Invalid age.') } this.#age = value }}En getter och setter anropas som om de vore en vanlig egenskap – myDog.age, inte myDog.age() – men implementationsmässigt är de funktioner. En getter tar aldrig emot argument (inget att skicka med vid hämtning); en setter tar alltid emot exakt ett argument (värdet som ska sättas) och kan innehålla godtycklig valideringslogik, inklusive att kasta ett undantag om värdet inte duger.
Har en klass bara en getter (ingen setter) för ett fält blir egenskapen skrivskyddad (“read-only”) – ett försök att tilldela den ett nytt värde ger ett tydligt fel istället för att tyst misslyckas.
Tärningen, tredje försöket
Section titled “Tärningen, tredje försöket”Med privata fält och getters/setters kan tärningsklassen skyddas fullt ut:
export class Die { #faceValue #maxFaceValue
constructor (maxFaceValue = 6) { this.maxFaceValue = maxFaceValue this.roll() }
get faceValue () { return this.#faceValue }
get maxFaceValue () { return this.#maxFaceValue }
set maxFaceValue (value) { if (!Number.isInteger(value) || value < 1) { throw new Error('Invalid number of faces.') } this.#maxFaceValue = value }
roll () { this.#faceValue = Math.floor(Math.random() * this.#maxFaceValue) + 1 }}faceValue har bara en getter – kan bara läsas, aldrig sättas direkt (bara via roll()). maxFaceValue har både getter och setter, men settern validerar värdet innan det accepteras. En getter/setter-kombination som bara returnerar respektive tilldelar fältets värde rakt av, utan någon egen logik, ger däremot inget skydd alls – då är det lika bra att ha en publik egenskap.
En viktig varning: referenstyper i privata fält
Section titled “En viktig varning: referenstyper i privata fält”Mönstret ovan fungerar problemfritt så länge det privata fältet är en värdetyp (som number). Är fältet istället en referenstyp (till exempel en array) räcker det inte att bara returnera fältet direkt i en getter – då lämnas referensen ut, och den som anropar kan muterar det “privata” innehållet ändå (en så kallad “privacy leak”). I det fallet behöver gettern istället returnera en kopia av objektet, annars är inkapslingen meningslös.
Beräknade egenskaper
Section titled “Beräknade egenskaper”Ibland finns det ingen underliggande fält alls bakom en getter – värdet som returneras kan istället vara beräknat från flera andra fält (till exempel en area beräknad från bredd och höjd). Det finns ingen anledning att lagra ett sådant värde separat när det ändå kan räknas fram vid behov.
Återanvändning av kod kan ske på många olika sätt. Här behandlas mekanismen “Arv”.
Upprepning av kod
Section titled “Upprepning av kod”Två klasser, Person och Student, kan hamna med exakt samma kod – samma egenskaper (name, age) och samma toString-metod, plus en eller två extra delar i Student. Behöver det vara så? Nej – med hjälp av arv (“inheritance”) kan kod återanvändas istället för att upprepas.
Återanvändning av kod genom arv
Section titled “Återanvändning av kod genom arv”Genom att låta Student ärva från Person med extends slipper Student duplicera det som redan finns i Person:
export class Person { constructor (name, age) { this.name = name this.age = age }
toString () { return `Jag, ${this.name}, är ${this.age} år.` }}
// student.jsimport { Person } from './person.js'
export class Student extends Person { constructor (name, age, onCampus) { super(name, age) this.onCampus = onCampus }
toString () { const form = this.onCampus ? 'campus' : 'distans' return `Jag, ${this.name}, läser på ${form}, och är ${this.age} år.` }}Person sägs vara basklass till Student, och Student utökar basklassen. Tumregeln för när arv passar: en Student är en Person (aldrig tvärtom) – men en Student är till exempel inte en hund, även om en hypotetisk Dog-klass också råkade ha name och age. Det gemensamma måste vara en genuin “är en”-relation, inte bara råkat ha samma egenskaper.
super(name, age) i konstruktorn anropar basklassens konstruktor för att initiera det den redan vet hur den ska initiera – annars måste Student duplicera det arbetet. super måste anropas innan this används i en konstruktor som ärver, och görs det så gott som alltid när arv används. Students egen toString överskuggar (“overrides”) den som ärvts från Person – anropas toString på ett Student-objekt är det alltid den mest specifika versionen (närmast i kedjan) som körs. Utelämnas den egna toString helt används istället Persons version.
Klassdiagram
Section titled “Klassdiagram”Ett klassdiagram (enligt standarden UML) är ett sätt att beskriva klasser och deras relationer utan att behöva rita alla de faktiska prototypreferenserna. Det består av tre fält: klassens namn överst, dess egenskaper i mitten, och dess metoder längst ner. En pil mellan två klasser visar arv – att den ena ärver från (är en) den andra.
Arv under huven
Section titled “Arv under huven”Bakom kulisserna byggs faktiskt två separata prototypkedjor upp när Student ärver från Person: en instansprototypkedja (en instans → Student.prototype → Person.prototype → Object.prototype) och en klassprototypkedja (Student → Person → Function.prototype, eftersom klasser i grunden är funktioner). Det är den förstnämnda som är intressant i praktiken. Klassdiagrammet ovan säger däremot ingenting om detta – det är en enklare, mer lättarbetad representation, men det är viktigt att komma ihåg att JavaScript gör betydligt mer under huven än vad diagrammet visar.
instanceof
Section titled “instanceof”Operatorn instanceof avgör om ett värde är en instans av en given klass, genom att följa just prototypkedjan:
const person = new Person('Nisse', 42)const student = new Student('Ellen', 23, true)
console.log(person instanceof Person) // OUTPUT: trueconsole.log(person instanceof Student) // OUTPUT: false – Nisse är ingen studentconsole.log(student instanceof Person) // OUTPUT: true – en student är en personconsole.log(student instanceof Student) // OUTPUT: trueinstanceof blir särskilt användbart längre fram i kursen vid felhantering: när ett undantag fångas i ett catch-block kan man fråga felobjektet vilken specifik feltyp det är, och hantera olika typer av fel på olika sätt.
Validering och strukturerad felhantering
Section titled “Validering och strukturerad felhantering”Att validera exempelvis strängar, kontrollera att de är skrivna på ett visst format, är mycket vanligt. Denna inspelning tittar på reguljära uttryck och ser om de kan hjälpa (eller stjälpa?) dig.
Validera en egenskaps värde
Section titled “Validera en egenskaps värde”Att kontrollera att en egenskap är korrekt formaterad – till exempel att ett svenskt postnummer består av fem siffror, eventuellt med ett mellanslag mellan tredje och fjärde siffran – kan göras med en handskriven kontroll i en setter, tecken för tecken:
set postalCode (value) { let valid = false if (value.length === 5 || (value.length === 6 && value.indexOf(' ') === 3)) { let i = 0 for (; i < value.length; i++) { if (value[i] === ' ') continue if (value[i] < '0' || value[i] > '9' || (i === 0 && value[i] === '0')) break } valid = i >= 5 && i <= 6 } if (!valid) { throw new Error('Invalid postal code.') } this._postalCode = value}Det fungerar, men blir snabbt både svårt att skriva korrekt och tungt att underhålla – och en enda ny regel (t.ex. att även sexsiffriga postnummer utan mellanslag ska godkännas) kräver att man river upp hela logiken igen.
Reguljära uttryck till undsättning
Section titled “Reguljära uttryck till undsättning”Ett reguljärt uttryck är ett mönster som beskriver kombinationer av tecken i en sträng, skrivet mellan snedstreck (/mönster/). Samma valideringslogik som ovan kan uttryckas i en enda rad:
set postalCode (value) { if (!/^[1-9]\d{2} ?\d{2}$/.test(value)) { throw new Error('Invalid postal code.') } this._postalCode = value}Uttrycket /^[1-9]\d{2} ?\d{2}$/ läses del för del: ^ markerar start av strängen, [1-9] exakt en siffra 1–9 (inte 0, eftersom postnummer aldrig börjar på 0), \d{2} exakt två siffror, ⎵? noll eller ett mellanslag, \d{2} ytterligare två siffror, och $ slutet av strängen. Ska även sexsiffriga postnummer (utan krav på mellanslag) godkännas räcker det att byta ut det sista \d{2} mot \d{2,3} – bara den delen behöver ändras.
Reguljära uttryck finns i de flesta programmeringsspråk (Python, Java, C# m.fl.), och det finns gott om webbaserade verktyg (till exempel regex101 och ExtendsClass Regex Tester) för att testa och förstå ett uttryck interaktivt innan det klistras in i koden. Mer finns att läsa på MDN under “Regular expressions”.
Kommandoradargument
Section titled “Kommandoradargument”En applikation kan göras mer generell genom att ta emot kommandoradargument när den startar (node src/app.js ett två 3 4). Node gör listan med argument – argumentvektorn – tillgänglig som en array via process.argv. De två första elementen (sökvägen till node.exe och sökvägen till applikationsfilen) är oftast ointressanta och hoppas över med Array#slice:
const add = function (a, b) { return a + b}
const myArgs = process.argv.slice(2)const sum = add(Number(myArgs[0]), Number(myArgs[1]))Ett alternativt, kortare sätt att plocka ut de två första relevanta argumenten och samtidigt konvertera dem till tal:
const [first, second] = process.argv.slice(2).map(Number)const sum = add(first, second)Avsluta en Node-applikation
Section titled “Avsluta en Node-applikation”Går allt bra sätter Node automatiskt applikationens avslutningskod till 0 när den avslutas. Kastas ett undantag som inte hanteras avslutas applikationen istället med avslutningskod 1. Det går att bestämma en egen avslutningskod innan applikationen avslutas – vanligtvis genom att sätta process.exitCode till ett värde och låta Node avsluta applikationen på ett kontrollerat sätt (att tvinga fram ett direkt avslut med process.exit() är oftast onödigt och bör undvikas):
try { main()} catch (err) { console.error(err.message) process.exitCode = 1
if (err instanceof MyError) { process.exitCode = 123 }}Genom att kontrollera vilken feltyp som fångades (med instanceof, se förra veckans genomgång av arv) kan olika fel resultera i olika avslutningskoder – något som blir relevant längre fram när automatiska tester ska tolka om en körning lyckades eller misslyckades.