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.
- Target: Dit is het originele object dat je wilt inpakken. Het kan elk soort object zijn, inclusief arrays, functies of zelfs een andere proxy. De proxy virtualiseert dit target en alle bewerkingen worden uiteindelijk (hoewel niet noodzakelijkerwijs) ernaar doorgestuurd.
- Handler: Dit is een object dat de logica voor de proxy bevat. Het is een placeholder-object waarvan de eigenschappen functies zijn, bekend als 'traps'. Wanneer een bewerking op de proxy plaatsvindt, zoekt het naar een overeenkomstige trap op de handler.
- Traps: Dit zijn de methoden op de handler die eigendomstoegang bieden. Elke trap komt overeen met een fundamentele objectbewerking. De
get
trap onderschept bijvoorbeeld het lezen van eigenschappen en deset
trap onderschept het schrijven van eigenschappen. Als een trap niet is gedefinieerd op de handler, wordt de bewerking eenvoudigweg doorgestuurd naar de target alsof de proxy er niet was.
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.
target
: Het originele object.property
: De naam van de eigenschap die wordt benaderd.receiver
: De proxy zelf, of een object dat ervan erft.
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.
value
: De nieuwe waarde die aan de eigenschap wordt toegekend.- De trap moet een boolean retourneren:
true
als de toewijzing succesvol was, enfalse
anders (wat eenTypeError
zal gooien in strict mode).
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.
target
: De originele functie.thisArg
: Dethis
context voor de aanroep.argumentsList
: De lijst met argumenten die aan de functie worden doorgegeven.
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)
- Goed voor: Het bouwen van frameworks en libraries (bijv. state management, ORM's), debugging en logging, het implementeren van robuuste validatiesystemen en het creëren van krachtige API's die onderliggende datastructuren abstraheren.
- Overweeg alternatieven voor: Prestatie kritische algoritmen, eenvoudige objectextensies waar een class of een factory functie voldoende zou zijn, of wanneer u zeer oude browsers moet ondersteunen die geen ES6 ondersteuning hebben.
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.
- Scope: `defineProperty` werkt op een per-eigenschap basis. U moet een getter/setter definiëren voor elke eigenschap die u wilt bekijken. De
get
enset
traps van een Proxy zijn globaal en vangen bewerkingen op elke eigenschap op, inclusief nieuwe die later worden toegevoegd. - Mogelijkheden: Proxies kunnen een breder scala aan bewerkingen onderscheppen, zoals
deleteProperty
, dein
operator en functie aanroepen, wat `defineProperty` niet kan.
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.