Arrays en objecten

Deze site is grotendeels een vertaling van een tutorial van Allison Parrish en is vertaald en bewerkt door Marijn van der Meer voor het gebruik bij Informatica lessen op het IJburg College

In dit hoofdstuk komen nog twee heel belangrijke onderwerpen bij het programmeren ter sprake. Je zal leren hoe je informatie van het gedrag van de gebruiker kan opslaan in je sketch. Om dit te kunnen doen moet je twee nieuwe programmeerconcepten leren die in iedere programmeertaal gebruikt worden: arrays en objecten.

Het doel van dit hoofdstuk is om een sketch te maken met het volgende gedrag: wanneer de gebruiker op de sketch klikt wordt er een rechthoek gecreeërd. En iedere rechthoek daalt dan langzaam tot het uit het beeld verdwijnt.

Overal rechthoeken

Je hebt al gezien hoe blijvende rechthoeken worden getekend. Je kan p5.js gewoon vertellen om niet iedere frame de achtergrond opnieuw te laten tekenen:

We weten ook hoe we een rechthoek moeten laten bewegen over het scherm. Met een beetje nadenken en uitproberen lukt het waarschijnlijk ook wel om de rechthoek te laten beginnen op het punt waar met de muis wordt geklikt:

(Het is de moeite waard om even de tijd te nemen om de bovenstaande code goed te begrijpen. Desnoods vraag je even aan een klasgenoot of de docent extra uitleg. De tekst hieronder wacht wel!)

Dit is mooi zal je zeggen, maar we zijn nog wel ver van ons doel: meerdere bewegende rechthoeken op het scherm laten verschijnen als reactie op input van de gebruiker. Dit is uiteraard te doen als we al meerdere rechthoeken zouden hebben vantevoren:

Maar dit staat in conflict met onze regel voor het programmeren: "voorkom dat je jezelf (en vooral code) herhaald", maar het werkt.

Een array van coordinaten

Denk eens even na hoe je de herhaling in het bovenstaande voorbeeld zou kunnen voorkomen, en hoe we het makkelijker zouden kunnen maken om meerdere rechthoeken in de sketch toe te voegen (dit is weer een stap naar ons einddoel, het maken van rechthoeken die door de gebruiker worden getekend).

Eerder hebben we een for loop gebruikt om herhaling te voorkomen en op het eerste gezicht lijkt het nu ook wel handig om for loop te gebruiken voor deze taak. Iets als:

// let op: dit is geen echte javascriptcode! Dit heet pseudocode...
for (var i = 0; i < 3; i++) {
    rect(100, rectY_<i>, 50, 25);
    rectY_<i> += 1
}

Javascript, en alle andere programmertalen, heeft een bepaalde data structuur die ontworpen is voor precies dit, en deze heet een array.

Een array een waarde waarin een lijst van andere waardes wordt bewaard. Om weer de lade analogie te gebruiken. Een array is een lade met vakjes waarin ook weer waarden in gestopt kunnen worden. Je kan weer bij al die individuele waarden door de positie die ze hebben in de lijst. Je kan ook waarden toevoegen, of juist weggooien of waarden aanpassen die er al in staan. Dus alle vakjes in de lade hebben een nummer van 0 tot het aantal waarden dat je erin hebt gestopt.

Hier het voorbeeld van hierboven, maar nu met een array:

Dit is heel veel nieuwe informatie! Laten we de code stap voor stap doornemen. Als eerste is er de declaratie (het aanmaken) van de array:

var rectY = [0, 15, 30];

Wanneer je een array aanmaakt in Javascript kan je de waarden direct in de array typen in je programma (dit heet een array literal; we gaan het later hebben over andere manier om arrays in je programma te maken). Het statement hierboven creeert een array en bewaart het in een variabele met de naam rectY.

Alle arrays hebben een .length attribuut die het aantal items in de array teruggeeft. In de eerste regel van de for loop gebruiken we deze om de bovengrens van de loop te bepalen:

for (var i = 0; i < rectY.length; i++) {

In de loop, evalueert de volgende expressie:

rectY[i]

… tot de waarde die bewaart wordt bij een bepaalde index van de array. (Een index is de plaats die het getal inneemt in de array, beginnend bij nul. Dus bij var rectY = [0, 15, 30]; geeft rectY[0] het eerste getal uit de array terug, 0 in dit geval. rectY[1] geeft 15 en rectY[2] geeft 30). Dus, in een loop, tekent dit statement:

rect((i+1)*100, rectY[i], 50, 25);

… een rechthoek dat gebruik maakt van de waarde die is opgeslagen op de specifieke index van de array (0, 1, of 2). Deze indexwaarde kan ook aangepast worden, met de onderstaande regel:

rectY[i] += 1;

Arrays in meer detail

Hierboven worden veel mogelijkheden van arrays in p5.js besproken. Maar het is de moeite waard om iets dieper in te gaan in het gebruik van een array. We zullen weer even beginnen met een lege sketch.

Om een array te maken begonnen we met vierkante haakjes met waarden daartussen gescheiden door komma's:

var stuff = [5, 10, 15, 20];

Een array kan zoveel waarden herbergen als je maar wilt. Het kan zelfs helemaal leeg zijn:

var helemaalNietsInHier = [];

Een array heeft een bepaald aantal waarden in zich. Om te zien hoeveel zagen we hierboven al dat je de .length attribuut konden gebruiken:

console.log(stuff.length); // geeft 4
console.log(helemaalNietsInHier.length); // geeft 0

Om een specifiek item uit een array te halen schrijf je vierkante haakjes direct achter de naam van de array en tussen die haakjes schrijf je dan het getal dat de positie weergeeft van dat item:

console.log(stuff[2]); // geeft 15 ... waarom niet 10?! Zie hieronder

Als je een getal invoert dat een positie vraagt dat buiten de lengte van de array ligt, dan zal je een undefined (ongedefinieerd) terug krijgen:

console.log(stuff[152]); // geeft "undefined"

In de console.log() functie kan je ook de naam van een array schrijven als parameter, je krijgt dan alle inhoud van een array. Dat kan zeer behulpzaam zijn bij het debuggen:

console.log(stuff); // geeft [5,10,15,20]

Je kan de waarde van een bepaalde positie (index) in een array veranderen door de naam van de array met tussen de vierkante haakjes de positie van de waarde die je wilt veranderen gevolgt door de toewijzingsoperator (=) gevolgd door de nieuwe waarde:

stuff[2] = 999;
console.log(stuff); // geeft nu [5,10,999,20]

De nul-index

Maar wacht eens even, waarom verwijst stuff[2] naar 15? Dat is toch al het derde item in de array en niet het tweede? Daar heb je zeker gelijk in. Maar de index (positie) begint bij nul. Misschien in het begin wat raar, maar bij vrijwel alle programmeertalen is dat zo.

Gebaseerd op nul betekent dat het eerste element van een array een index van 0 heeft in Javascript. Dus als je het eerste element uit het array stuff wilt halen, moet je stuff[0] schrijven. En om het laatste element uit een array van vier elementen te halen moet je stuff[3] schrijven (en dus niet stuff[4]).

De reden voor deze rare gewoonte ligt in de historie van het programmeren Wikipedia heeft een goede uitleg over de geschiedenis van het nul-indexen.

Als je er gewoon vanuit gaat dat de index in een array de afstand van het begin weergeeft en niet het aantal elementen telt dat er in een array zitten wordt dit wat makkelijker om te onthouden. Het eerste element is dus nul stappen van het begin verwijderd; het tweede element is een stap verwijderd, enzovoort. Maar je zal dit in het begin gewoon wel eens fout doen, dus doet je code niet wat je dacht dat het moest doen en gebruik je een array... controleer even of je de juiste index hebt gebruikt.

Elementen toevoegen aan een array

Arrays hebben een methode (dat is een functie die specifiek hoort bij, in dit geval, een array) genaamd .push(). Deze voegt items toe aan de lijst. In het volgende voorbeeld gebruiken we deze methode om eindelijk tot iets te komen wat ongeveer lijkt op ons eigenlijke doel: een vallende rechthoek toevoegen als de gebruiker klikt:

Het bijhouden van meerdere eigenschappen

Eindelijk! We hebben bijna ons doel bereikt: als we op de sketch klikken wordt er een rechthoek toegevoegd die vervolgens naar beneden beweegt. Maar het is niet precies wat we bedoelde, toch? Laten we ons doel verder leggen door iets toe te voegen: als je op het scherm klikt verschijnt de rechthoek exact op de coordinaten van de muisklik en beweegt vervolgens naar beneden.

Om dit te doen moeten we twee dingen bijhouden voor iedere rechthoek. Dit wordt interessant want er zijn meerdere manieren om dit te doen met allemaal zijn voor- en nadelen. We gaan ze allemaal om de beurt toepassen.

Meer dan één array

De makkelijkste oplossing is misschien wel het maken van een array om de X-coordinaten van iedere rechthoek op te slaan, en een array voor de Y-coordinaten. De X van de eerste rechthoek bevindt zich op index 0 van de rectX array, en de Y waarde van de eerste rechthoek bevindt zich op de index 0 van de rectY array, enzovoort. Zo ziet dat er uit:

Dit ziet er goed uit! Maar er zijn wat nadelen als je je code wilt uitbreiden:

  • Om een andere attribuut toe te voegen aan de data die we opslaan voor iedere rechthoek moeten we een derde array maken (en vierde en vijfde...)
  • Om een "rechthoek" te verwijderen uit onze data, moeten we dat item uit alle arrays halen. (Later meer over het verwijderen van data uit een array.)
  • De relatie tussen rectX en rectY wordt niet weergegeven in de syntax van het programma. Een andere programmeur die naar de code kijkt zal niet onmiddellijk zien dat beide variabelen altijd samen gebruikt dienen te worden.

Arrays in een array

Een andere oplossing komt voort uit het feit dat arrays zelf gewoon waarden zijn, en kunnen we dus een array in een andere array bewaren. Om dit te zien type eens de volgende code in een lege sketch en kijk eens naar de output in het console:

var stuff = [];
stuff.push([24, 25]);
stuff.push([26, 27]);
console.log(stuff); // geeft [[24,25],[26,27]]

Als je deze code uitvoert, bestaat de variabele stuff uit een array met twee items. En beide items zijn zelf ook weer een array. Als je de waarde van index 0 (de eerste positie) vraagt, krijg je een array terug:

console.log(stuff[0]); // geeft [24,25]

Om nu weer een van de twee waarden van deze array te krijgen moet je twee keer vierkante haakjes gebruiken. De eerste keer om de array uit de array te halen en de tweede om uit die gekozen array een waarde te halen die daar in staat, snap je het nog? Zo ziet dat eruit:

console.log(stuff[0][1]); // De eerste array en dan de tweede waarde geeft 25

We kunnen deze structuur gebruiken om een nieuwe versie van onze vallende-rechthoeken-sketch te maken met behulp van maar een array om de informatie op te slaan van alle rechthoeken. Elk element van dat array is zelf ook weer een array.

Het mooie van deze constructie is dat we nu makkelijk een derde attribuut kunnen toevoegen bij iedere rechthoek door een derde element toe te voegen aan de array die we al toevoegen aan de rectXY array bij iedere muisklik. In het volgende voorbeeld wordt een random waarde toegevoegd op index 2 (derde positie) bij iedere muisklik. Deze waarde wordt gebruikt om iedere rechthoek een willekeurige kleur te geven in de draw() functie:

OEFENING: Probeer een vierde element toe te voegen aan de array dat de valsnelheid van iedere rechthoek bepaalt.

Objecten

Het nadeel van arrays-in-arrays is dat je al snel vergeet wat de elementen in de binnenste arrays ook al weer betekenden. De getallen zelf zeggen niet zo veel (waarom is de X-coordinaat opgeslagen op index 0? Wat heeft 'X' te maken met '0'?). Zou het niet handig zijn als er een manier in Javascript was om data in een datastructuur te stoppen dat lijkt op een array in die zin dat het meerdere waarden kan bevatten, maar dan met de mogelijkheid om de elementen een begrijpbare naam te geven.

Er blijkt uiteraard zo'n datastructuur te bestaan! (anders was ik er vast niet over begonnen). Het heet een object. Een object kan meerdere waarden bevatten, net zoals een array, maar alle individuele waarden kunnen een key krijgen, een naam. Objects zijn verder net als iedere andere waarde in Javascript: je kan ze in variabelen opslaan, ze in arrays stoppen, of zelfs objecten in objecten opslaan.

De basis syntax om een object te creëren ziet er zo uit:

var asteroide = {radius: 100, massa: 460, populatie: 17};

Dit statement creëert een object en koppelt deze aan de variabele asteroide. Dit object heeft drie “keys”: radius, massa, and populatie. (Je kan zelf de namen van de keys verzinnen; ik kies hier alleen voor de lol een ruimtereis-thema-object.) Om nu toegang te krijgen tot een waarde bij een specifieke key, gebruik je een van de volgende syntaxen:

console.log(asteroide["radius"]);
// of:
console.log(asteroide.radius)

Je kan een nieuwe key/value paar toevoegen aan een object door de syntax van hierboven te gebruiken voor het "=-teken" en de waarde die je wilt toevoegen aan de rechterkant:

asteroide.albedo = 7;
// of:
asteroide["albedo"] = 7;

Het console.log() commando zal alle key/value paren die in een object zitten laten zien als je een objectnaam als parameter invoert:

// geeft {"radius":100,"massa":460,"populatie":17,"albedo":7}
console.log(asteroide);

Het is ook makkelijk om een array van objecten te maken en dit de meest gebruikte manier van het gebruiken van objecten. Je zal het veelvuldig gebruiken in je net begonnen carriere als computerprogrammeur. Meestal zal je arrays inladen van een bron van buiten (zoals een API), of je zal een object laten genereren gedurende je programma (bijvoorbeeld voor het opslaan van data van gebruikersinput). Maar je kan ook direct een eigen object schrijven, zo ziet dat eruit:

var katten = [
  {leeftijd: 14, gewicht: 12.2},
  {leeftijd: 3, gewicht: 8.9},
  {leeftijd: 8, gewicht: 11.0}
];

Nu is katten een array van objecten. Om één object uit deze array te halen moet je een expressie schrijven als:

console.log(katten[0]); // geeft {"leeftijd":14,"gewicht":12.2}

En om de waarde van een key uit een object te krijgen in de lijst schrijf je:

console.log(katten[0]["leeftijd"]);
// of
console.log(katten[0].leeftijd);

Hier is een voorbeeld van hoe je een loop maakt om door een lijst van objecten heen te gaan. Deze loop print de som van het gewicht van alle katten uit de lijst:

var gewichtSom = 0;
for (var i = 0; i < katten.length; i++) {
    gewichtSom += katten[i].gewicht;
}
// geeft 32.1
console.log(gewichtSom);

Rechthoeken als objecten

Hier is een versie van onze rechthoek sketch die alles bij elkaar brengt. Op iedere klik creeert deze sketch een nieuw object en stopt deze in de rectObjs array. In draw(), wordt iedere rechthoek getoond door gebruik te maken van de waarden met de juiste keys:

OEFENING: Pas het bovenste voorbeeld aan zodat iedere rechthoek ook een aparte snelheid for de X- en Y-coordinaten op slaat. Pas vervolgens de draw() loop aan zodat de rechthoeken ook bewegen in een andere richting dan naar beneden. BONUSOEFENING: schrijf de code zo dat iedere rechthoek "stuitert" op de rand als het de randen van de sketch raakt.

Gebruik maken van andermans data (in CSV formaat)

Niet alle data die je gebruikt hoef je zelf te maken of te laten genereren in je sketch. Je kan p5.js ook gebruiken om data in te laden van andere databronnen. Ik zal je nu laten zien hoe je met data werkt in het CSV formaat.

Plekken om data te vinden:

We gaan werken met een CSV met de data van alle Ajax competitie wedstrijden van het seizoen 2017-2018.

“CSV” staat voor “comma separated values” (komma gescheiden waarden). Het is een platte tekst bestand (dus geen opmaak, zoals word of google documenten) dat een data in een vaste structuur bevat. Het is eigenlijk gewoon een spreadsheet waarin de namen van de kolommen op de eerste regel van het bestand staan. Iedere volgende "rij" staat op een volgende regel met aparte cellen die door komma's worden gescheiden. Het CSV formaat wordt door heel veel programmeertalen begrepen, maar ook Excel of Google Spreadsheets weten raad met dit formaat. Veel organisaties en bedrijven gebruiken het CSV formaat om data te delen met anderen. Dus er zijn veel bronnen te vinden online waar je CSV kan downloaden.

Eigenlijk is een CSV-bestand een soort van array van rijen, en iedere rij is een object waarvan de key de kolomnaam is en de waarden in de cellen in die kolom staan.

De p5.js library geeft een heel aantal functies die horen bij het het werken met CSV-bestanden. De loadTable() functie laat een CSV-bestand in het geheugen als een bijzonder soort tabel waarde. De loadTable() functie moet in een preload() worden ingeladen; de syntax ziet er zo uit:

tabelVar = loadTable('je_bestand.csv', 'csv', 'header');

De tabelVar variabele kan wederom genoemd worden zoals je wilt. De 'csv' en 'header' parameters vertellen de loadTable() dat dit een CSV-bestand is en dat het "header" waarden heeft op de eerste regel (die gebruikt worden als kolomnamen).

De tabel waarde heeft verschillende methodes die we kunnen gebruiken om met de ingelezen data te werken:

  • .getRowCount(): geeft het aantal rijen in de tabel weer
  • .getNum(i, "foo"): geeft de waarde van de cel in de rij i met de kolomnaam "foo"

(Er zijn meer functies om data uit andere formaten te halen; zie de referentie voor meer informatie.)

Hier is een voorbeeld dat het Ajax CSV-bestand gebruikt om de scores weer te geven per wedstrijd van Ajax. Vertikale lijnen geven de wedstrijden aan (waarbij rode lijnen UIT-wedstrijden zijn en witte THUIS-wedstrijden). De horizontale lijnen zijn het aantal doelpunten een rode cirkel is het aantal doelpunten van Ajax en de witte cirkels de doelpunten van de tegenpartij: