JavaScript ES6 Features: de belangrijkste vernieuwingen uitgelegd
Inleiding
ES6 (ook bekend als ES2015) is een grote update van JavaScript die de taal moderner, expressiever en beter onderhoudbaar maakt. Veel patronen die vroeger omslachtig waren (zoals functies binden, string-concatenatie, prototype-gedoe, callback-hell) kregen met ES6 een duidelijkere syntaxis of betere bouwstenen.
In deze tutorial leer je de belangrijkste ES6-vernieuwingen diepgaand kennen, met praktische voorbeelden, valkuilen en echte commando’s om zelf te testen. De focus ligt op code die je vandaag nog kunt gebruiken in Node.js en moderne browsers.
Voorbereiding: snel testen met Node.js
Node-versie controleren
Open een terminal en controleer je Node.js-versie:
node -v
ES6 wordt al lang breed ondersteund, maar een recente Node-versie is prettig. Als je Node nog niet hebt:
- Installeer via je pakketbeheer (Linux)
- Of via de officiële installer (Windows/macOS)
Een klein testbestand maken en uitvoeren
Maak een bestand es6-demo.js:
mkdir es6-tutorial
cd es6-tutorial
touch es6-demo.js
node es6-demo.js
Je kunt ook de Node-REPL gebruiken:
node
1) let en const: block scope en betere intentie
Probleem met var
var is function-scoped, niet block-scoped. Dat leidt tot verrassingen:
function voorbeeldVar() {
if (true) {
var x = 10;
}
console.log(x); // 10, ook buiten het blok
}
Met ES6 krijg je let en const, die block-scoped zijn:
function voorbeeldLet() {
if (true) {
let x = 10;
}
// console.log(x); // ReferenceError: x is not defined
}
const betekent niet “immutable”
const betekent: de binding kan niet opnieuw worden toegewezen. Het object zelf kan nog steeds muteren:
const user = { naam: "Sam" };
user.naam = "Alex"; // toegestaan
// user = { naam: "Kim" }; // TypeError: Assignment to constant variable
Wil je immutabiliteit, dan kun je bijvoorbeeld Object.freeze gebruiken (met beperkingen: het is “shallow”):
const settings = Object.freeze({ theme: "dark" });
// settings.theme = "light"; // faalt stil in non-strict, of TypeError in strict
Best practice
- Gebruik standaard
const. - Gebruik
letals je echt opnieuw moet toewijzen. - Vermijd
varin moderne codebases.
2) Arrow functions: korter, en vooral: lexicale this
Arrow functions zijn compacter:
const som = (a, b) => a + b;
console.log(som(2, 3)); // 5
Lexicale this (belangrijkste winst)
Bij gewone functies hangt this af van hoe je de functie aanroept. Arrow functions nemen this over uit de omliggende scope.
Voorbeeld met een klassiek probleem:
function Teller() {
this.waarde = 0;
setInterval(function () {
this.waarde++;
console.log(this.waarde);
}, 1000);
}
new Teller();
Dit werkt niet zoals je verwacht, omdat this in de callback niet naar de instance wijst.
Oplossing met arrow function:
function Teller() {
this.waarde = 0;
setInterval(() => {
this.waarde++;
console.log(this.waarde);
}, 1000);
}
new Teller();
Valkuil: arrow functions als methods
Arrow functions zijn meestal geen goede keuze als objectmethod, omdat je vaak juist dynamische this wilt:
const obj = {
waarde: 42,
toon: () => {
console.log(this.waarde);
},
};
obj.toon(); // meestal undefined, want this is niet obj
Gebruik dan een gewone method:
const obj2 = {
waarde: 42,
toon() {
console.log(this.waarde);
},
};
obj2.toon(); // 42
3) Template literals: leesbare strings en multiline
Vroeger:
const naam = "Nora";
const zin = "Hallo " + naam + ", welkom!";
Met template literals:
const naam = "Nora";
const zin = `Hallo ${naam}, welkom!`;
Multiline is direct mogelijk:
const bericht = `
Beste ${naam},
Dit is een bericht over meerdere regels.
Groeten!
`;
console.log(bericht);
Expressies in ${...}
Je kunt elke expressie gebruiken:
const a = 5;
const b = 7;
console.log(`Som: ${a + b}`); // Som: 12
4) Destructuring: sneller waarden uit objecten en arrays halen
Object destructuring
const user = { id: 1, naam: "Aylin", rol: "admin" };
const { naam, rol } = user;
console.log(naam, rol);
Hernoemen:
const { naam: displayName } = user;
console.log(displayName);
Default values:
const { taal = "nl" } = user;
console.log(taal); // nl (want user.taal ontbreekt)
Diep destructuren:
const data = { profiel: { contact: { email: "a@b.nl" } } };
const {
profiel: {
contact: { email },
},
} = data;
console.log(email);
Array destructuring
const punten = [10, 20, 30];
const [eerste, tweede] = punten;
console.log(eerste, tweede);
Overslaan en rest:
const [a, , c] = punten;
console.log(a, c); // 10 30
const [head, ...tail] = punten;
console.log(head); // 10
console.log(tail); // [20, 30]
Praktisch: destructuring in functieparameters
function printUser({ naam, rol = "user" }) {
console.log(`${naam} (${rol})`);
}
printUser({ naam: "Bo" }); // Bo (user)
5) Default parameters: minder boilerplate
Vroeger:
function begroet(naam) {
naam = naam || "vriend";
return "Hallo " + naam;
}
Met ES6:
function begroet(naam = "vriend") {
return `Hallo ${naam}`;
}
Let op: default wordt alleen gebruikt bij undefined, niet bij null:
console.log(begroet(undefined)); // Hallo vriend
console.log(begroet(null)); // Hallo null
6) Rest en spread: flexibele argumenten en kopieën
Rest parameters
function telOp(...getallen) {
return getallen.reduce((acc, n) => acc + n, 0);
}
console.log(telOp(1, 2, 3, 4)); // 10
Rest moet als laatste parameter staan.
Spread voor arrays
Arrays samenvoegen:
const a = [1, 2];
const b = [3, 4];
const samen = [...a, ...b];
console.log(samen); // [1,2,3,4]
Kopie maken (shallow):
const origineel = [{ x: 1 }, { x: 2 }];
const kopie = [...origineel];
kopie[0].x = 99;
console.log(origineel[0].x); // 99 (shallow copy!)
Spread voor objecten
Objecten combineren:
const basis = { host: "localhost", port: 3000 };
const override = { port: 8080 };
const config = { ...basis, ...override };
console.log(config); // port is 8080
Let op: bij conflicts wint de laatste.
7) Enhanced object literals: minder herhaling, betere definities
Property shorthand
const naam = "Iris";
const leeftijd = 29;
const persoon = { naam, leeftijd };
console.log(persoon);
Method shorthand
const reken = {
som(a, b) {
return a + b;
},
};
console.log(reken.som(2, 5));
Computed property names
const key = "status";
const obj = {
[key]: "actief",
};
console.log(obj.status);
8) Classes: syntactische suiker bovenop prototypes
ES6 classes maken OOP-achtige code leesbaarder, maar onder de motorkap blijft het prototype-gebaseerd.
Basis class
class Gebruiker {
constructor(naam) {
this.naam = naam;
}
begroet() {
return `Hallo, ik ben ${this.naam}`;
}
}
const g = new Gebruiker("Milan");
console.log(g.begroet());
Overerving met extends en super
class Admin extends Gebruiker {
constructor(naam, rechten) {
super(naam);
this.rechten = rechten;
}
isSuperAdmin() {
return this.rechten.includes("root");
}
}
const a = new Admin("Lina", ["read", "root"]);
console.log(a.begroet());
console.log(a.isSuperAdmin());
Getter en setter
class Temperatuur {
constructor(celsius) {
this._c = celsius;
}
get celsius() {
return this._c;
}
set celsius(v) {
if (typeof v !== "number") throw new TypeError("Moet een getal zijn");
this._c = v;
}
get fahrenheit() {
return this._c * 9 / 5 + 32;
}
}
const t = new Temperatuur(20);
console.log(t.fahrenheit);
t.celsius = 25;
console.log(t.fahrenheit);
Belangrijke nuance
- Class-declaraties worden niet “gehoist” zoals function declarations.
- Methods zijn niet automatisch gebonden;
thiskan verloren gaan als je methods los doorgeeft.
9) Modules: import en export (basis)
ES6 introduceert een standaardsysteem voor modules. In browsers werkt dit met <script type="module">. In Node.js werkt dit met ESM, vaak via .mjs of "type": "module" in package.json.
Node.js project met ESM opzetten
mkdir es6-modules
cd es6-modules
npm init -y
Pas package.json aan en voeg toe:
{
"type": "module"
}
Exporteren en importeren
Maak math.js:
export function som(a, b) {
return a + b;
}
export const PI = 3.14159;
export default function kwadraat(x) {
return x * x;
}
Maak index.js:
import kwadraat, { som, PI } from "./math.js";
console.log(som(2, 3));
console.log(PI);
console.log(kwadraat(6));
Run:
node index.js
Named vs default exports
- Named export: je importeert met dezelfde naam (of via
as). - Default export: je kiest zelf de naam bij import.
10) Promises: beter omgaan met async code
Vroeger deed je veel met callbacks. Promises geven structuur: een async operatie is “pending”, en wordt “fulfilled” of “rejected”.
Een Promise maken
function wacht(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve(`Klaar na ${ms}ms`), ms);
});
}
wacht(500).then(console.log);
Fouten afhandelen met .catch
function faal() {
return new Promise((_, reject) => {
reject(new Error("Er ging iets mis"));
});
}
faal()
.then(() => console.log("Dit gebeurt niet"))
.catch((err) => console.error("Fout:", err.message));
Promise chaining
wacht(200)
.then((msg) => {
console.log(msg);
return wacht(200);
})
.then((msg) => {
console.log("Tweede:", msg);
});
Parallel uitvoeren met Promise.all
Promise.all([wacht(200), wacht(300), wacht(100)])
.then((resultaten) => console.log(resultaten));
Let op: Promise.all faalt meteen als één Promise reject.
11) Map en Set: betere datastructuren dan “plain objects” voor bepaalde taken
Set: unieke waarden
const s = new Set();
s.add("a");
s.add("a");
s.add("b");
console.log(s.size); // 2
console.log(s.has("a")); // true
console.log([...s]); // ["a","b"]
Praktisch: duplicaten verwijderen:
const lijst = [1, 1, 2, 3, 3, 3];
const uniek = [...new Set(lijst)];
console.log(uniek); // [1,2,3]
Map: sleutel-waarde met elke sleutelsoort
Objecten als sleutel:
const m = new Map();
const keyObj = { id: 1 };
m.set(keyObj, "waarde voor object-sleutel");
console.log(m.get(keyObj));
console.log(m.has(keyObj));
Itereren:
for (const [k, v] of m) {
console.log(k, v);
}
Waarom Map?
- Sleutels kunnen elk type zijn (ook objecten).
- Betere voorspelbaarheid bij iteratie.
- Geen botsing met prototype-properties.
12) Iterators en for...of: nette iteratie over iterables
for...of werkt met iterables zoals arrays, strings, maps, sets:
for (const ch of "test") {
console.log(ch);
}
Met arrays:
const arr = ["x", "y", "z"];
for (const item of arr) {
console.log(item);
}
Verschil met for...in:
for...initereert over keys (en kan ook geërfde keys meenemen).for...ofitereert over waarden van een iterable.
13) Generators: functies die kunnen pauzeren
Generators zijn functies die je kunt pauzeren en hervatten met yield. Ze leveren een iterator op.
function* teller(max) {
let i = 1;
while (i <= max) {
yield i;
i++;
}
}
const it = teller(3);
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
Itereren met for...of:
for (const n of teller(5)) {
console.log(n);
}
Wanneer nuttig?
- Lazy sequences (grote reeksen zonder alles in geheugen te laden)
- Complexe iteratie-logica
- Basis voor bepaalde async patronen (historisch), al is dat tegenwoordig vaak vervangen door
async/await(later toegevoegd, niet ES6)
14) Symbol: unieke property keys
Symbol maakt unieke identifiers, vaak gebruikt om property names te vermijden die botsen.
const geheim = Symbol("geheim");
const obj = {
[geheim]: 123,
publiek: 456,
};
console.log(obj.publiek); // 456
console.log(obj[geheim]); // 123
Symbols verschijnen niet standaard in Object.keys:
console.log(Object.keys(obj)); // ["publiek"]
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(geheim)]
15) Nieuwe string- en array-methodes (selectie)
Strings
console.log("Hallo".startsWith("Ha")); // true
console.log("Hallo".endsWith("lo")); // true
console.log("Hallo".includes("all")); // true
console.log("ha".repeat(3)); // "hahaha"
Arrays
Array.from:
const set = new Set([1, 2, 3]);
console.log(Array.from(set)); // [1,2,3]
find en findIndex:
const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
const u = users.find((x) => x.id === 2);
console.log(u); // { id: 2 }
const idx = users.findIndex((x) => x.id === 3);
console.log(idx); // 2
16) Praktische mini-case: configuratie, destructuring, defaults, spread, modules
Hier combineren we meerdere ES6-features in één klein voorbeeld.
Stap 1: project opzetten
mkdir es6-mini-case
cd es6-mini-case
npm init -y
Zet ESM aan in package.json:
{
"type": "module"
}
Stap 2: config.js
const defaultConfig = {
host: "127.0.0.1",
port: 3000,
features: {
logging: true,
cache: false,
},
};
export function maakConfig(overrides = {}) {
// shallow merge voor top-level
const merged = { ...defaultConfig, ...overrides };
// handmatige merge voor nested object (features), ook shallow
merged.features = { ...defaultConfig.features, ...overrides.features };
return merged;
}
export default defaultConfig;
Stap 3: index.js
import defaultConfig, { maakConfig } from "./config.js";
const cliOverrides = {
port: 8080,
features: { cache: true },
};
const config = maakConfig(cliOverrides);
// destructuring met defaults
const {
host,
port,
features: { logging, cache },
} = config;
console.log("Default:", defaultConfig);
console.log(`Server start op ${host}:${port}`);
console.log("logging:", logging, "cache:", cache);
Run:
node index.js
Wat je hier leert:
export defaulten named exports- spread voor object-merge
- destructuring, inclusief nested destructuring
- default parameters in
maakConfig
17) Veelgemaakte fouten en aandachtspunten
1) Shallow copies met spread
Spread ({...obj} en [...arr]) kopieert alleen de bovenste laag. Geneste objecten blijven dezelfde referentie houden. Als je “deep copy” nodig hebt, moet je bewust kiezen voor een strategie, bijvoorbeeld:
- gestructureerde cloning (waar beschikbaar)
- een eigen deep-copy functie
- een bibliotheek
2) this en arrow functions
- Arrow functions zijn ideaal voor callbacks waar je
thiswilt behouden. - Gebruik geen arrow function als objectmethod wanneer je dynamische
thisverwacht.
3) const geeft geen immutabiliteit
const beschermt tegen her-toewijzing, niet tegen mutatie. Combineer met immutabele patronen als dat je doel is.
4) Modules en padnamen
In ESM moet je in Node doorgaans de extensie meegeven:
import { som } from "./math.js";
Zonder .js werkt het vaak niet zoals je verwacht.
18) Samenvatting: wat ES6 je vooral oplevert
Met ES6 krijg je:
- Betere scope-regels (
let,const) - Leesbaardere functies en minder
this-problemen (arrow functions) - Expressieve syntax (template literals, destructuring, defaults, rest/spread)
- Moderne structuren (classes, modules)
- Sterkere async-bouwstenen (Promises)
- Betere collecties (
Map,Set) - Nettere iteratie (
for...of, iterators, generators) - Nieuwe primitieve (
Symbol) voor unieke keys
Als je deze features consequent toepast, schrijf je code die korter, duidelijker en robuuster is, en die beter schaalbaar blijft in grotere projecten.
Oefeningen (aanrader)
- Vervang in een bestaand script alle
vardoorlet/consten los scope-bugs op. - Schrijf een module
logger.jsmet een default export en minstens twee named exports. - Maak een functie
mergeDeepdie in elk geval één niveau diep objecten merge’t (zoals bijfeaturesin de mini-case). - Bouw een kleine
Set-gebaseerde deduplicatie voor een lijst met e-mails en valideer metincludes,startsWith,endsWith. - Schrijf een generator die paginagewijs resultaten oplevert (bijvoorbeeld arrays van 10 items per
yield).
Als je wilt, kun je jouw huidige code of een klein fragment delen; dan kan ik aanwijzen welke ES6-features het meest winst opleveren en hoe je ze veilig refactort.