De `Object`-klasse in JavaScript

De `Object`-klasse in JavaScript

Dit artikel legt de Object-klasse in JavaScript uit.

Dit artikel legt de Object-klasse in JavaScript uit, inclusief praktische voorbeelden.

YouTube Video

De Object-klasse in JavaScript

Object is een ingebouwd object dat als basis dient voor alle JavaScript-objecten. Veel van de kernfuncties van de taal, zoals het beheren van eigenschappen, overerving (prototypeketen), enumeratie, klonen en bevriezen, worden mogelijk gemaakt door het gedrag van Object.

Object.create

Er zijn meerdere manieren om objecten te maken, en je dient deze passend te gebruiken afhankelijk van je doel.

Objectliteral (meest gebruikelijk)

De volgende code toont de eenvoudigste en meest leesbare manier om een object aan te maken.

 1// Create an object using object literal
 2const user = {
 3  name: "Alice",
 4  age: 30,
 5  greet() {
 6    return `Hello, I'm ${this.name}`;
 7  }
 8};
 9
10console.log(user.greet()); // "Hello, I'm Alice"
  • In dit voorbeeld worden eigenschappen en methoden gedefinieerd met behulp van letterlijke waarden (literals). Het is eenvoudig en biedt doorgaans betere prestaties.

new Object()-constructor

De Object-constructor wordt zelden gebruikt, maar het is nuttig om zijn gedrag te begrijpen.

1// Create an object using the Object constructor
2const objFromCtor = new Object();
3objFromCtor.x = 10;
4objFromCtor.y = 20;
5
6console.log(objFromCtor); // { x: 10, y: 20 }
  • new Object() retourneert een leeg object, maar de literal {} is korter en gebruikelijker.

Het prototype specificeren met Object.create

Object.create wordt gebruikt om een object met een gespecificeerd prototype te maken.

1// Create an object with a specified prototype
2const proto = { hello() { return "hi"; } };
3const obj = Object.create(proto);
4obj.name = "Bob";
5
6console.log(obj.hello()); // "hi"
7console.log(Object.getPrototypeOf(obj) === proto); // true
  • Object.create is ideaal voor op overerving gebaseerde objectontwerpen, waardoor je nauwkeurig controle hebt over de prototypeketen.

Eigenschap attributen en descriptors

Eigenschappen hebben attributen zoals 'value', 'writable', 'enumerable' en 'configurable', welke je in detail kunt beheren met Object.defineProperty.

Basisvoorbeeld van het gebruik van defineProperty

Hieronder volgt een voorbeeld van het definiëren van niet-listeerbare en alleen-lezen eigenschappen met behulp van defineProperty.

 1// Define a non-enumerable read-only property
 2const person = { name: "Carol" };
 3
 4Object.defineProperty(person, "id", {
 5  value: 12345,
 6  writable: false,
 7  enumerable: false,
 8  configurable: false
 9});
10
11console.log(person.id); // 12345
12console.log(Object.keys(person)); // ["name"] — "id" is non-enumerable
13person.id = 999; // silently fails or throws in strict mode
14console.log(person.id); // still 12345
  • Met defineProperty kun je het gedrag van eigenschappen, zoals opsomming, overschrijven en verwijderen, precies bepalen.

Accessor-eigenschappen (getter / setter)

Met accessors kun je logica toevoegen aan het lezen en schrijven van eigenschappen.

 1// Use getter and setter to manage internal state
 2const data = {
 3  _value: 1,
 4  get value() {
 5    return this._value;
 6  },
 7  set value(v) {
 8    if (typeof v === "number" && v > 0) {
 9      this._value = v;
10    } else {
11      throw new Error("value must be a positive number");
12    }
13  }
14};
15
16console.log(data.value); // 1
17data.value = 5;
18console.log(data.value); // 5
19// data.value = -1; // would throw
  • Met getter en setter kun je eigenschaps toegang behandelen als een externe API en validatie of neveneffecten toevoegen.

Prototype en overerving (prototype / __proto__ / Object.getPrototypeOf)

Overerving in JavaScript is gebaseerd op de prototypeketen, niet op klassen. Objecten kunnen naar andere objecten verwijzen als hun prototypes.

Object.getPrototypeOf en Object.setPrototypeOf

Het volgende voorbeeld laat zien hoe je prototypes kunt bekijken en instellen.

1// Inspect and change prototype
2const base = { speak() { return "base"; } };
3const derived = Object.create(base);
4console.log(Object.getPrototypeOf(derived) === base); // true
5
6const other = { speak() { return "other"; } };
7Object.setPrototypeOf(derived, other);
8console.log(derived.speak()); // "other"
  • Object.getPrototypeOf haalt het prototype van een object op.
  • Object.setPrototypeOf wijzigt het prototype van een bestaand object, maar je moet dit voorzichtig gebruiken omdat het de prestaties kan beïnvloeden.

Belangrijke ingebouwde methoden

We leggen duidelijk de meest gebruikte en belangrijkste methoden uit, geselecteerd uit de instantiefuncties van Object.prototype, evenals de statische methoden die Object bezit.

hasOwnProperty, isPrototypeOf, toString, valueOf

hasOwnProperty, isPrototypeOf, toString en valueOf bepalen het basisgedrag van objecten.

 1// Demonstrate prototype methods
 2const base = { greet() { return "hello"; } };
 3const child = Object.create(base);
 4const a = { x: 1 };
 5
 6console.log(a.hasOwnProperty("x")); // true
 7console.log(a.hasOwnProperty("toString")); // false — toString is inherited
 8
 9console.log(a.toString()); // "[object Object]" by default
10
11console.log(base.isPrototypeOf(child)); // true
12console.log(Object.prototype.isPrototypeOf(child)); // true
  • hasOwnProperty is een essentiële methode om te controleren of een eigenschap direct op het object zit.
  • isPrototypeOf controleert of het doelobject zichzelf als prototype heeft.

Object.keys, Object.values, Object.entries

Object.keys, Object.values en Object.entries retourneren lijsten van de eigen enumerateerbare eigenschappen van een object. Deze zijn handig voor iteratie en transformatie.

 1// Keys, values and entries
 2const item = { id: 1, name: "Widget", price: 9.99 };
 3
 4// ["id", "name", "price"]
 5console.log(Object.keys(item));
 6
 7// [1, "Widget", 9.99]
 8console.log(Object.values(item));
 9
10// [["id",1], ["name","Widget"], ["price",9.99]]
11console.log(Object.entries(item));
  • Deze worden vaak gebruikt voor het itereren en transformeren van objecten.

Object.assign

Object.assign wordt gebruikt voor het oppervlakkig kopiëren en samenvoegen van objecten. Let op dat prototypes en accessor-eigenschappen niet worden gekopieerd.

1// Shallow copy / merge using Object.assign
2const target = { a: 1 };
3const source = { b: 2 };
4const result = Object.assign(target, source);
5
6console.log(result); // { a: 1, b: 2 }
7console.log(target === result); // true (merged into target)
  • Bij geneste objecten worden alleen de referenties gekopieerd, dus je hebt een andere implementatie nodig voor diep klonen.

Object.freeze, Object.seal, Object.preventExtensions

Object.freeze, Object.seal en Object.preventExtensions regelen de mutabiliteit van objecten.

 1// Freeze vs seal vs preventExtensions
 2const obj = { a: 1 };
 3Object.freeze(obj);
 4obj.a = 2; // fails silently or throws in strict mode
 5delete obj.a; // fails
 6
 7const obj2 = { b: 2 };
 8Object.seal(obj2);
 9obj2.b = 3; // allowed
10// delete obj2.b; // fails
11
12const obj3 = { c: 3 };
13Object.preventExtensions(obj3);
14obj3.d = 4; // fails
  • freeze is het strengst; het voorkomt dat er wijzigingen worden aangebracht aan de eigenschappen van het object.
  • seal voorkomt het toevoegen of verwijderen van eigenschappen, maar staat toe bestaande waarden te wijzigen.
  • preventExtensions voorkomt alleen het toevoegen van nieuwe eigenschappen; bestaande eigenschappen kunnen nog steeds gewijzigd of verwijderd worden.

Object-enumeratie, volgorde en for...in / for...of

for...in enumereert de benamingen van eigenschappen, maar bevat ook eigenschappen uit de prototypeketen, daarom wordt het vaak samen met hasOwnProperty gebruikt. Het combineren van Object.keys() met for...of is veiliger en maakt je bedoelingen duidelijker.

 1// Safe enumeration
 2const obj = Object.create({ inherited: true });
 3obj.own = 1;
 4
 5for (const key in obj) {
 6  if (obj.hasOwnProperty(key)) {
 7    console.log("own prop:", key);
 8  } else {
 9    console.log("inherited prop:", key);
10  }
11}
12
13for (const key of Object.keys(obj)) {
14  console.log("key via Object.keys:", key);
15}
  • De regels voor het enumereren van eigenschappen zijn vastgelegd in de ECMAScript-specificatie, en soms is de enumeratievolgorde gegarandeerd, soms niet. Als algemene regel worden sleutels die als nummer worden geïnterpreteerd in oplopende volgorde geplaatst, terwijl andere sleutels de invoegvolgorde volgen.

Klonen en diep kopiëren

Er zijn twee soorten objectkopieën: oppervlakkige kopieën met Object.assign of de spread-syntaxis, en diepe kopieën met recursieve methoden. Het is belangrijk om ze op de juiste manier te gebruiken, afhankelijk van de situatie.

Oppervlakkig kopiëren (spread-syntax / Object.assign)

1// Shallow copy with spread operator
2const original = { a: 1, nested: { x: 10 } };
3const shallow = { ...original };
4shallow.nested.x = 99;
5console.log(original.nested.x); // 99 — nested object is shared
  • Bij oppervlakkig kopiëren delen geneste objecten referenties, dus wijzigingen in het originele object kunnen de kopie beïnvloeden.

Eenvoudig diep kopiëren (met kanttekeningen)

Het zogenaamde JSON-trucje is een snelle manier om een diepe kopie te maken, maar heeft nadelen zoals het verliezen van functies, Date, circulaire referenties en undefined-waarden. Voor echt diep klonen moet je een gespecialiseerde bibliotheek gebruiken.

1// Deep clone using JSON methods — limited use-cases only
2const source = { a: 1, d: new Date(), nested: { x: 2 } };
3const cloned = JSON.parse(JSON.stringify(source));
4console.log(cloned); // ok for plain data, but Date becomes string, functions lost
  • JSON-gebaseerde methoden zijn handig voor het snel verwerken van eenvoudige gegevens, maar in veelvoorkomende gevallen kan hun werking verkeerd uitpakken.

Mixins en objectcompositie

In plaats van meervoudige overerving wordt vaak het patroon van gedragssamenstelling met mixins gebruikt.

 1// Simple mixin function
 2const canEat = {
 3  eat() { return `${this.name} eats`; }
 4};
 5const canWalk = {
 6  walk() { return `${this.name} walks`; }
 7};
 8
 9function createPerson(name) {
10  const person = { name };
11  return Object.assign(person, canEat, canWalk);
12}
13
14const p = createPerson("Dana");
15console.log(p.eat()); // "Dana eats"
16console.log(p.walk()); // "Dana walks"
  • Mixins zijn flexibel, maar je moet oppassen voor naamconflicten en testbaarheid.

Veelvoorkomende valkuilen en beste praktijken

Hier zijn enkele veelvoorkomende valkuilen en beste praktijken.

  • Mutabiliteit Objecten zijn standaard veranderlijk (mutable). Overweeg in applicaties met toestandsbeheer het gebruik van onveranderlijke (immutable) datastructuren via Object.freeze of een immutable-bibliotheek.

  • Prototype-vervuiling Het direct samenvoegen van externe data met een object via Object.assign of lussen kan onverwachte bijwerkingen veroorzaken met speciale eigenschappen zoals __proto__ of constructor, wat beveiligingsrisico's oplevert. Filter gebruikersinvoer voordat je deze direct samenvoegt.

  • Valkuilen van for...in for...in enumereert ook prototype-eigenschappen, dus controleer met hasOwnProperty. Object.keys gebruiken is duidelijker.

  • Misbruik van oppervlakkig kopiëren Overweeg of je diep kopiëren nodig hebt om te voorkomen dat wijzigingen in geneste objecten het originele object beïnvloeden.

Praktisch Voorbeeld: Onveranderlijk Object Updatepatroon

Patronen die een nieuw object retourneren zonder direct de toestand te wijzigen, worden vaak gebruikt in React en vergelijkbare bibliotheken.

 1// Immutable update example
 2const state = { todos: [{ id: 1, text: "Buy milk", done: false }] };
 3
 4// Toggle todo done immutably
 5function toggleTodo(state, todoId) {
 6  return {
 7    ...state,
 8    todos: state.todos.map(t => t.id === todoId ? { ...t, done: !t.done } : t)
 9  };
10}
11
12const newState = toggleTodo(state, 1);
13console.log(state.todos[0].done); // false
14console.log(newState.todos[0].done); // true
  • Deze code is een voorbeeld van het aanmaken van een nieuw statusobject zonder het originele state-object direct te wijzigen. De functie toggleTodo kopieert de todos-array en retourneert een nieuw object waarin alleen het doel-element is aangepast, zodat het originele state ongewijzigd blijft.
  • Onveranderlijke updates verminderen bijwerkingen en maken toestandsbeheer eenvoudiger.

Praktisch Voorbeeld: Veilig Samenvoegen (Let op Prototype-vervuiling)

Negeer __proto__ bij het samenvoegen van externe JSON om prototype-vervuiling te voorkomen.

 1// Safe merge ignoring __proto__ keys
 2function safeMerge(target, source) {
 3  for (const key of Object.keys(source)) {
 4    if (key === "__proto__" || key === "constructor") continue;
 5    target[key] = source[key];
 6  }
 7  return target;
 8}
 9
10const target = {};
11const source = JSON.parse('{"a":1,"__proto__":{"polluted":true}}');
12safeMerge(target, source);
13console.log(target.polluted); // undefined — safe
14console.log({}.polluted); // undefined — prototype not polluted
  • Dit soort bescherming is ook belangrijk in bibliotheken en frameworks.

Prestatiefactoren

Wat prestaties betreft, houd rekening met het volgende:.

  • Vermijd veelvuldige prototypewijzigingen (Object.setPrototypeOf) of dynamisch toevoegen/verwijderen van eigenschappen, want dit belemmert optimalisaties door de engine.
  • Bij het genereren van veel kleine objecten wordt optimalisatie effectiever als je objecten gebruikt met een uniforme structuur (dezelfde set eigenschappen).
  • Diep kopiëren is kostbaar. Beperk het gebruik ervan of overweeg diff-gebaseerde updates.

Samenvatting

Object staat centraal in JavaScript en biedt allerlei mogelijkheden zoals objectcreatie, eigenschapsbeheer, overerving, kopiëren en mutabiliteitsbeheer. Het is belangrijk om API's zoals Object.defineProperty, Object.assign en Object.freeze te begrijpen, en zorgvuldig te ontwerpen om valkuilen zoals prototypevervuiling en oppervlakkige kopieën te vermijden.

Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.

YouTube Video