Nederlands

Een uitgebreide gids voor globale ontwikkelaars over het beheersen van de JavaScript Proxy API. Leer objectbewerkingen onderscheppen en aanpassen met praktische voorbeelden, use cases en prestatie tips.

JavaScript Proxy API: Een diepgaande duik in objectgedragsmodificatie

In het evoluerende landschap van moderne JavaScript zijn ontwikkelaars constant op zoek naar krachtigere en elegantere manieren om data te beheren en ermee te interageren. Hoewel functies zoals classes, modules en async/await een revolutie teweeg hebben gebracht in de manier waarop we code schrijven, is er een krachtige metaprogrammeerfunctie die is geïntroduceerd in ECMAScript 2015 (ES6) die vaak onderbenut blijft: de Proxy API.

Metaprogrammeren klinkt misschien intimiderend, maar het is simpelweg het concept van het schrijven van code die op andere code werkt. De Proxy API is JavaScript's belangrijkste tool hiervoor, waarmee je een 'proxy' kunt maken voor een ander object, die fundamentele bewerkingen voor dat object kan onderscheppen en herdefiniëren. Het is alsof je een aanpasbare poortwachter voor een object plaatst, waardoor je volledige controle hebt over hoe het wordt benaderd en gewijzigd.

Deze uitgebreide gids zal de Proxy API ontrafelen. We zullen de kernconcepten ervan verkennen, de verschillende mogelijkheden ervan opsplitsen met praktische voorbeelden en geavanceerde use cases en prestatieoverwegingen bespreken. Tegen het einde zul je begrijpen waarom Proxies een hoeksteen zijn van moderne frameworks en hoe je ze kunt gebruiken om schonere, krachtigere en beter onderhoudbare code te schrijven.

De kernconcepten begrijpen: Target, Handler en Traps

De Proxy API is gebouwd op drie fundamentele componenten. Het begrijpen van hun rollen is de sleutel tot het beheersen van proxies.

De syntax voor het maken van een proxy is eenvoudig:

const proxy = new Proxy(target, handler);

Laten we eens kijken naar een heel eenvoudig voorbeeld. We maken een proxy die simpelweg alle bewerkingen doorgeeft aan het target object door een lege handler te gebruiken.


// Het originele object
const target = {
  message: "Hello, World!"
};

// Een lege handler. Alle bewerkingen worden doorgestuurd naar de target.
const handler = {};

// Het proxy-object
const proxy = new Proxy(target, handler);

// Toegang tot een eigenschap op de proxy
console.log(proxy.message); // Output: Hello, World!

// De bewerking is doorgestuurd naar de target
console.log(target.message); // Output: Hello, World!

// Een eigenschap wijzigen via de proxy
proxy.anotherMessage = "Hello, Proxy!";

console.log(proxy.anotherMessage); // Output: Hello, Proxy!
console.log(target.anotherMessage); // Output: Hello, Proxy!

In dit voorbeeld gedraagt de proxy zich precies zoals het originele object. De echte kracht komt wanneer we traps beginnen te definiëren in de handler.

De anatomie van een Proxy: het verkennen van veelvoorkomende traps

Het handler-object kan maximaal 13 verschillende traps bevatten, die elk overeenkomen met een fundamentele interne methode van JavaScript-objecten. Laten we de meest voorkomende en nuttige onderzoeken.

Eigendomstoegangstraps

1. `get(target, property, receiver)`

Dit is misschien wel de meest gebruikte trap. Het wordt geactiveerd wanneer een eigenschap van de proxy wordt gelezen.

Voorbeeld: Standaardwaarden voor niet-bestaande eigenschappen.


const user = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30
};

const userHandler = {
  get(target, property) {
    // Als de eigenschap bestaat op de target, retourneer deze.
    // Anders retourneert u een standaardbericht.
    return property in target ? target[property] : `Property '${property}' does not exist.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Output: John
console.log(userProxy.age);       // Output: 30
console.log(userProxy.country);   // Output: Property 'country' does not exist.

2. `set(target, property, value, receiver)`

De set trap wordt aangeroepen wanneer aan een eigenschap van de proxy een waarde wordt toegekend. Het is perfect voor validatie, logging of het maken van alleen-lezen objecten.

Voorbeeld: Data validatie.


const person = {
  name: 'Jane Doe',
  age: 25
};

const validationHandler = {
  set(target, property, value) {
    if (property === 'age') {
      if (typeof value !== 'number' || !Number.isInteger(value)) {
        throw new TypeError('Age must be an integer.');
      }
      if (value <= 0) {
        throw new RangeError('Age must be a positive number.');
      }
    }

    // Als de validatie slaagt, stel dan de waarde in op het target object.
    target[property] = value;

    // Succes aangeven.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // Dit is valide
console.log(personProxy.age); // Output: 30

try {
  personProxy.age = 'thirty'; // Gooit TypeError
} catch (e) {
  console.error(e.message); // Output: Age must be an integer.
}

try {
  personProxy.age = -5; // Gooit RangeError
} catch (e) {
  console.error(e.message); // Output: Age must be a positive number.
}

3. `has(target, property)`

Deze trap onderschept de in operator. Hiermee kunt u bepalen welke eigenschappen op een object lijken te bestaan.

Voorbeeld: Het verbergen van 'private' eigenschappen.

In JavaScript is een gebruikelijke conventie om private eigenschappen te prefixen met een underscore (_). We kunnen de has trap gebruiken om deze te verbergen voor de in operator.


const secretData = {
  _apiKey: 'xyz123abc',
  publicKey: 'pub456def',
  id: 1
};

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Doe alsof het niet bestaat
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Output: true
console.log('_apiKey' in dataProxy);   // Output: false (ook al staat het op de target)
console.log('id' in dataProxy);        // Output: true

Opmerking: dit heeft alleen invloed op de in operator. Directe toegang zoals dataProxy._apiKey zou nog steeds werken, tenzij u ook een overeenkomstige get trap implementeert.

4. `deleteProperty(target, property)`

Deze trap wordt uitgevoerd wanneer een eigenschap wordt verwijderd met behulp van de delete operator. Het is handig om te voorkomen dat belangrijke eigenschappen worden verwijderd.

De trap moet true retourneren voor een succesvolle verwijdering of false voor een mislukte.

Voorbeeld: Het voorkomen van het verwijderen van eigenschappen.


const immutableConfig = {
  databaseUrl: 'prod.db.server',
  port: 8080
};

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Attempted to delete protected property: '${property}'. Operation denied.`);
      return false;
    }
    return true; // Eigenschap bestond toch al niet
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Console output: Attempted to delete protected property: 'port'. Operation denied.

console.log(configProxy.port); // Output: 8080 (Het is niet verwijderd)

Object Enumeratie en Beschrijvingstraps

5. `ownKeys(target)`

Deze trap wordt geactiveerd door bewerkingen die de lijst met eigen eigenschappen van een object ophalen, zoals Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() en Reflect.ownKeys().

Voorbeeld: Sleutels filteren.

Laten we dit combineren met ons eerdere voorbeeld van 'private' eigenschappen om ze volledig te verbergen.


const secretData = {
  _apiKey: 'xyz123abc',
  publicKey: 'pub456def',
  id: 1
};

const keyHidingHandler = {
  has(target, property) {
    return !property.startsWith('_') && property in target;
  },
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(key => !key.startsWith('_'));
  },
  get(target, property, receiver) {
    // Ook directe toegang voorkomen
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

console.log(Object.keys(fullProxy)); // Output: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // Output: true
console.log('_apiKey' in fullProxy);   // Output: false
console.log(fullProxy._apiKey);      // Output: undefined

Merk op dat we hier Reflect gebruiken. Het Reflect object biedt methoden voor onderschepbare JavaScript bewerkingen, en de methoden hebben dezelfde namen en signaturen als de proxy traps. Het is een best practice om Reflect te gebruiken om de originele bewerking naar de target door te sturen, zodat het standaardgedrag correct wordt gehandhaafd.

Functie- en Constructorstraps

Proxies zijn niet beperkt tot platte objecten. Wanneer de target een functie is, kun je aanroepen en constructies onderscheppen.

6. `apply(target, thisArg, argumentsList)`

Deze trap wordt aangeroepen wanneer een proxy van een functie wordt uitgevoerd. Het onderschept de functie aanroep.

Voorbeeld: Functie aanroepen en hun argumenten loggen.


function sum(a, b) {
  return a + b;
}

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Calling function '${target.name}' with arguments: ${argumentsList}`);
    // Voer de originele functie uit met de juiste context en argumenten
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`Function '${target.name}' returned: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Console output:
// Calling function 'sum' with arguments: 5,10
// Function 'sum' returned: 15

7. `construct(target, argumentsList, newTarget)`

Deze trap onderschept het gebruik van de new operator op een proxy van een class of functie.

Voorbeeld: Singleton patroon implementatie.


class MyDatabaseConnection {
  constructor(url) {
    this.url = url;
    console.log(`Connecting to ${this.url}...`);
  }
}

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Creating new instance.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Returning existing instance.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Console output:
// Creating new instance.
// Connecting to db://primary...
// Returning existing instance.

const conn2 = new ProxiedConnection('db://secondary'); // URL will be ignored
// Console output:
// Returning existing instance.

console.log(conn1 === conn2); // Output: true
console.log(conn1.url); // Output: db://primary
console.log(conn2.url); // Output: db://primary

Praktische use cases en geavanceerde patronen

Nu we de individuele traps hebben behandeld, laten we eens kijken hoe ze kunnen worden gecombineerd om problemen uit de echte wereld op te lossen.

1. API Abstractie en Datatransformatie

API's retourneren vaak data in een formaat dat niet overeenkomt met de conventies van uw applicatie (bijv. snake_case vs. camelCase). Een proxy kan deze conversie transparant afhandelen.


function snakeToCamel(s) {
  return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}

// Stel je voor dat dit onze ruwe data is van een API
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // Controleer of de camelCase versie direct bestaat
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Terugvallen op de originele eigenschapsnaam
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// We kunnen nu eigenschappen benaderen met behulp van camelCase, ook al zijn ze opgeslagen als snake_case
console.log(userModel.userId);        // Output: 123
console.log(userModel.firstName);     // Output: Alice
console.log(userModel.accountStatus); // Output: active

2. Observables en Databinding (De kern van moderne frameworks)

Proxies vormen de motor achter de reactiviteitssystemen in moderne frameworks zoals Vue 3. Wanneer u een eigenschap wijzigt van een proxied state object, kan de set trap worden gebruikt om updates in de UI of andere delen van de applicatie te activeren.

Hier is een sterk vereenvoudigd voorbeeld:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Activeer de callback bij wijziging
      return result;
    }
  };
  return new Proxy(target, handler);
}

const state = {
  count: 0,
  message: 'Hello'
};

function render(prop, value) {
  console.log(`CHANGE DETECTED: The property '${prop}' was set to '${value}'. Re-rendering UI...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Console output: CHANGE DETECTED: The property 'count' was set to '1'. Re-rendering UI...

observableState.message = 'Goodbye';
// Console output: CHANGE DETECTED: The property 'message' was set to 'Goodbye'. Re-rendering UI...

3. Negatieve Array Indices

Een klassiek en leuk voorbeeld is het uitbreiden van het native array gedrag om negatieve indices te ondersteunen, waarbij -1 verwijst naar het laatste element, vergelijkbaar met talen als Python.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Converteer negatieve index naar een positieve vanaf het einde
        property = String(target.length + index);
      }
      return Reflect.get(target, property);
    }
  };
  return new Proxy(arr, handler);
}

const originalArray = ['a', 'b', 'c', 'd', 'e'];
const proxiedArray = createNegativeArrayProxy(originalArray);

console.log(proxiedArray[0]);  // Output: a
console.log(proxiedArray[-1]); // Output: e
console.log(proxiedArray[-2]); // Output: d
console.log(proxiedArray.length); // Output: 5

Prestatieoverwegingen en Best Practices

Hoewel proxies ongelooflijk krachtig zijn, zijn ze geen wondermiddel. Het is cruciaal om hun implicaties te begrijpen.

De Prestatie Overhead

Een proxy introduceert een laag van indirectie. Elke bewerking op een proxied object moet door de handler gaan, wat een kleine overhead toevoegt in vergelijking met een directe bewerking op een plat object. Voor de meeste applicaties (zoals data validatie of framework-level reactiviteit) is deze overhead verwaarloosbaar. In prestatie kritische code, zoals een strakke loop die miljoenen items verwerkt, kan dit echter een bottleneck worden. Benchmark altijd als prestatie een primaire zorg is.

Proxy Invarianten

Een trap kan niet volledig liegen over de aard van het target object. JavaScript dwingt een reeks regels af die 'invarianten' worden genoemd en die proxy traps moeten gehoorzamen. Het overtreden van een invariant resulteert in een TypeError.

Een invariant voor de deleteProperty trap is bijvoorbeeld dat deze geen true kan retourneren (wat succes aangeeft) als de overeenkomstige eigenschap van het target object niet-configureerbaar is. Dit voorkomt dat de proxy beweert dat het een eigenschap heeft verwijderd die niet kan worden verwijderd.


const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });

const handler = {
  deleteProperty(target, prop) {
    // Dit zal de invariant schenden
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // Dit zal een fout gooien
} catch (e) {
  console.error(e.message);
  // Output: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}

Wanneer Proxies te Gebruiken (en wanneer niet)

Herroepbare Proxies

Voor scenario's waarin u mogelijk een proxy moet 'uitschakelen' (bijv. om veiligheidsredenen of geheugenbeheer), biedt JavaScript Proxy.revocable(). Het retourneert een object dat zowel de proxy als een revoke functie bevat.


const target = { data: 'sensitive' };
const handler = {};

const { proxy, revoke } = Proxy.revocable(target, handler);

console.log(proxy.data); // Output: sensitive

// Nu herroepen we de toegang van de proxy
revoke();

try {
  console.log(proxy.data); // Dit zal een fout gooien
} catch (e) {
  console.error(e.message);
  // Output: Cannot perform 'get' on a proxy that has been revoked
}

Proxies vs. Andere Metaprogrammeertechnieken

Vóór Proxies gebruikten ontwikkelaars andere methoden om vergelijkbare doelen te bereiken. Het is nuttig om te begrijpen hoe Proxies zich verhouden.

`Object.defineProperty()`

Object.defineProperty() wijzigt een object rechtstreeks door getters en setters voor specifieke eigenschappen te definiëren. Proxies daarentegen wijzigen het originele object helemaal niet; ze pakken het in.

Conclusie: De kracht van virtualisatie

De JavaScript Proxy API is meer dan alleen een slimme functie; het is een fundamentele verschuiving in de manier waarop we objecten kunnen ontwerpen en ermee kunnen interageren. Door ons in staat te stellen fundamentele bewerkingen te onderscheppen en aan te passen, openen Proxies de deur naar een wereld van krachtige patronen: van naadloze data validatie en transformatie tot de reactieve systemen die moderne gebruikersinterfaces aandrijven.

Hoewel ze een kleine prestatiekosten met zich meebrengen en een reeks regels die moeten worden gevolgd, is hun vermogen om schone, ontkoppelde en krachtige abstracties te creëren ongeëvenaard. Door objecten te virtualiseren, kunt u systemen bouwen die robuuster, beter onderhoudbaar en expressiever zijn. De volgende keer dat u een complexe uitdaging aangaat met betrekking tot databeheer, validatie of observeerbaarheid, overweeg dan of een Proxy de juiste tool is voor de klus. Het is misschien wel de meest elegante oplossing in uw toolkit.

JavaScript Proxy API: Een diepgaande duik in objectgedragsmodificatie | MLOG