Tried Proxy yet...?
Introduction
As the name suggests, a Proxy is an intermediary that acts on behalf of another entity to perform tasks or relay information.
I first encountered the power of Proxy while exploring the Vue.js reactivity model.This experience opened my eyes to its versatility and potential. Although modern frameworks are increasingly adopting Signals for reactivity, Proxy remains highly relevant and valuable.
When I first delved into JS Proxy, I was immediately captivated by its ease of usage, flexibility and potential use-cases. After grasping the basics, I eagerly applied it in my projects, discovering two distinct ways to enhance my work. Initially, I felt accomplished, but as I explored the broader community, I realized that I had only scratched the surface. The possibilities with JS Proxy extend far beyond what I had imagined.
In this article we will explore:
How Proxy acts as a shape-shifter?
How Proxy is better then setter/getters?
Some real-world practical use cases along with code examples
And much more!
What is a Proxy in JS?
A Proxy object allows you to create a wrapper around another object, intercepting and redefining fundamental operations for that object, such as property lookup, assignment, enumeration, function invocation, and others.
Description
Proxy can be viewed as an advanced extension of setters and getters
—a super-set that provides a broader scope of control over object operations. While getters and setters focus on intercepting access to specific properties, Proxy offers a comprehensive mechanism for intercepting a wide range of fundamental operations not just get, set but has, deleteProperty, apply and many more JavaScript operations.
Create a Proxy
const myProxy = new Proxy(target, handler);
A Proxy in JavaScript is created using two parameters:
target: The original object that you want to proxy.
handler: An object that specifies which operations on the
target
will be intercepted(using traps) and how these operations should be customized.
Code Example
The following code demonstrates how to create a Proxy for a target Object:
const person = {
firstName: 'John',
lastName: 'Doe',
};
const handler = {
get(target, prop) {
if (prop === 'firstName') {
return target[prop].toUpperCase();
}
return target[prop];
},
set(target, prop, value) {
if (prop === 'firstName') {
target[prop] = value.toUpperCase();
}
// Indicate success
return true;
},
};
const proxyUser = new Proxy(person, handler);
// invokes the `get` trap
console.log(proxyUser.firstName); // JOHN
console.log(proxyUser.lastName); // Doe
// invokes the `set` trap
proxyUser.firstName = 'jane'
console.log(proxyUser.firstName); // JANE
console.log(proxyUser.lastName); // Doe
Proxy vs Setter/Getter
You might wonder you can achieve the same result using setters and getters. So, what are the advantages of using a Proxy instead?
To understand this lets see how the above code will look if we use setter/getter
const person = {
_firstName: 'John', // this is not `firstName` notice the `_`
_lastName: 'Doe', // this is not `lastName` notice the `_`
// we can't use firstName or lastName as it will cause
// RangeError: Maximum call stack size exceeded
get firstName() {
return this._firstName.toUpperCase();
},
set firstName(value) {
this._firstName = value.toUpperCase();
},
get lastName() {
return this._lastName;
},
set lastName(value) {
this._lastName = value;
}
};
console.log(person.firstName); // JOHN
console.log(person.lastName); // Doe
person.firstName = 'jane'
console.log(person.firstName); // JANE
console.log(person.lastName); // Doe
After comparing both the examples, here are the some key advantages of using JavaScript Proxy over traditional getter/setter
methods:
We don't need to change the original object
We can re-use same logic for accessing different properties. With
setter/getter
we have to define a newset
andget
method for different propertiesWe can create multiple Proxies for the same object, each tailored for different use-cases. For example, we can create a different proxy that can return
firstName
incamel-case
. Another scenario where multiple proxies would be useful (e.g., different validation rules for different environments).Proxy is not limited to just
set
andget
, it can intercept other methods usingtraps
likedelete
andfunction invocations
, which aren't covered by traditional getters/setters.
What is a trap?
The function that define the behavior for the corresponding object internal method.(This is analogous to the concept of traps in operating systems.)
from MDN
In simple terms: Traps
(gatekeepers) are functions in the Proxy handler that define how operations (like getting or setting properties) are intercepted and customized.
In the above example, the handler defined two traps the set
and get
to intercept the [[GET]] and [[SET]] method of the target object. Similarly, Proxy supports other traps that can intercept other internal methods of the Object.
Here is a comprehensive list of traps supported by the Proxy:
get: Intercepts getting a property value.
set: Intercepts setting a property value.
has: Intercepts the
in
operator.deleteProperty: Intercepts the
delete
operator.apply: Intercepts function calls.
construct: Intercepts the
new
operator.getOwnPropertyDescriptor: Intercepts
Object.getOwnPropertyDescriptor
.defineProperty: Intercepts
Object.defineProperty
.getPrototypeOf: Intercepts
Object.getPrototypeOf
.setPrototypeOf: Intercepts
Object.setPrototypeOf
.isExtensible: Intercepts
Object.isExtensible
.preventExtensions: Intercepts
Object.preventExtensions
.ownKeys: Intercepts
Object.getOwnPropertyNames
,Object.getOwnPropertySymbols
, andObject.keys
.hasOwn: Intercepts
Object.hasOwn
(available starting ES2022).
The list above clearly defines the scope upto which proxy can be used to target and proxy different JS object internals. Each has their own kind of classic real world use case.
Proxy as a Shape-Shifter
Consider two JSONs mentioned below
// Config 1 - dualLanguageConfig
{
"configType": "dual",
"primary": {
"isDefault": true,
"languageCode": "en-US",
"supportedFormats": [
"text",
"html",
"markdown"
],
"translationConfig": {
"autoTranslate": true,
"provider": "GoogleTranslate",
"cacheDuration": 3600,
"fallbackLanguage": "en"
}
},
"secondary": {
"isDefault": false,
"languageCode": "es-ES",
"supportedFormats": [
"text",
"html"
],
"translationConfig": {
"autoTranslate": false,
"provider": "None",
"cacheDuration": 0,
"fallbackLanguage": "en"
}
}
}
// Config 2 - singleLanguageConfig
{
"configType": "single",
"primary": {
"isDefault": true,
"languageCode": "en-US",
"supportedFormats": [
"text",
"html",
"markdown"
],
"translationConfig": {
"autoTranslate": true,
"provider": "GoogleTranslate",
"cacheDuration": 3600,
"fallbackLanguage": "en"
}
},
"secondary": {
}
}
To access all the nested properties in such a JSON structure, you have two main options:
You can either write a wrapper module with functions for each property or
Use dot notation to directly access deeply nested properties like
config.secondary.translationConfig.autoTranslate
.
However, since the secondary
config is only defined when configType
is dual
, direct property access requires checking type first and then adding null checks. While you could build these checks into a wrapper function, a Proxy allows you to handle these scenarios seamlessly, enabling you to safely access properties without the need for additional function calls or manual checks.
const handleLanguageConfig = {
get(target, prop) {
const { type, primary, secondary } = target;
if (type === 'single') {
return primary[prop]
}
// handle all dual configurations
if (prop === 'supportedFormats') {
// return unique supported formats only
return [...new Set([...primary.supportedFormats, ...secondary.supportedFormats])];
}
return [primary, secondary].map(config => config[prop]);
},
};
const proxyConfigDual = new Proxy(dual, handleLanguageConfig);
const proxyConfigSingle = new Proxy(single, handleLanguageConfig);
console.log(proxyConfigDual.languageCode); // ["en-US", "es-ES"]
console.log(proxyConfigSingle.languageCode); // "en-US"
console.log(proxyConfigDual.supportedFormats); // ["text", "html", "markdown", "csv"]
console.log(proxyConfigSingle.supportedFormats); // ["text", "html", "csv"]
In the example above, we effortlessly manage property access based on type and conditionally process values as needed. The supportedFormats
property is accessed directly, yet we ensure unique values by handling its specific case conditionally.
Regardless of the config type provided, we automatically retrieve the correct values by simply accessing the property, rather than calling a wrapper function. The logic that determines the values based on type is encapsulated in an independent handler, handleLanguageConfig
. This approach allows us to create additional handlers and apply them with a proxy, all without altering the original config object.
const handleLanguageConfig = {
get(target, prop) {
const { type, primary, secondary } = target;
if (type === 'single') {
return primary[prop]
}
// handle all dual configurations
if (prop === 'supportedFormats') {
// return unique supported formats only
return [...new Set([...primary.supportedFormats, ...secondary.supportedFormats])];
}
return [primary, secondary].map(config => config[prop]);
},
};
const handleLanguageConfig2 = {
get(target, prop) {
const { type, primary, secondary } = target;
// handle all dual configurations
if (prop === 'supportedFormats') {
// return unique supported formats only
return [...new Set([...primary.supportedFormats, ...secondary.supportedFormats])];
}
return [primary, secondary].map(config => config[prop]);
},
};
const proxyConfigOld = new Proxy(single, handleLanguageConfig);
const proxyConfigNew = new Proxy(single, handleLanguageConfig2);
console.log(proxyConfigOld.languageCode); // "en-US"
// here we are getting languageCode in an Array as well
console.log(proxyConfigNew.languageCode); // ["en-US"]
By providing the appropriate proxy handler, we can override and customize the behavior of the proxied object.
Proxies in Action: Practical Use Cases
A Proxy can adjust its behavior based on the needs of the application. Whether it's controlling access to object properties, modifying data dynamically, or handling complex validation logic, a Proxy can transform how your objects interact with the rest of the system. This flexibility isn't just theoretical—it solves real-world problems developers face every day.
Below are some Proxy's Real world use cases:
Detect Change
Reactivity
Validate Input
Auditing and Logging
Protect Sensitive Data
Garbage Collection and Memory Management
Soft Deletion
Manage Defaults and Missing properties
Rate Limiter
Profiling
Access Nested Objects in a cleaner way
and many more you can think of...
Check this awesome list of what others have developed using proxy https://github.com/mikaelbr/awesome-es2015-proxy?tab=readme-ov-file#modules
Lets explore some code examples of the scenarios mentioned above
Code examples
Detect Change
const target = { name: 'John Doe', age: 30 }; const handler = { get: function (obj, prop) { console.log(`Property ${prop} accessed with value ${obj[prop]}`); return obj[prop]; }, set: function (obj, prop, value) { console.log(`Property ${prop} changed from ${obj[prop]} to ${value}`); obj[prop] = value; return true; }, deleteProperty: function (obj, prop) { console.log(`Property ${prop} deleted`); delete obj[prop]; return true; } }; const proxy = new Proxy(target, handler); // Accessing properties console.log(proxy.name); // Logs: Property name accessed with value John Doe // Changing properties proxy.age = 31; // Logs: Property age changed from 30 to 31 // Deleting properties delete proxy.name; // Logs: Property name deleted
get Trap: Logs a message when a property is accessed.
set Trap: Logs a message when a property is assigned a new value.
deleteProperty Trap: Logs a message when a property is deleted.
By using a Proxy
, you can effectively monitor and react to changes in an object's properties, making it a powerful tool for various use cases such as state management, debugging, and more.
Reactivity
// Function to update the DOM function updateDOM(state) { document.getElementById('name').textContent = `Name: ${state.name}`; document.getElementById('age').textContent = `Age: ${state.age}`; } // Initial state const state = { name: 'John Doe', age: 30 }; // Handler for the Proxy const handler = { set: function (obj, prop, value) { obj[prop] = value; updateDOM(obj); // Update the DOM whenever the state changes return true; } }; // Create a Proxy for the state const proxyState = new Proxy(state, handler); // Initial DOM update updateDOM(proxyState); // Event listeners for buttons document.getElementById('updateName').addEventListener('click', () => { proxyState.name = 'Jane Doe'; // This will trigger the DOM update }); document.getElementById('updateAge').addEventListener('click', () => { proxyState.age = 31; // This will trigger the DOM update });
By using a
Proxy
, you can create a simple reactive system that automatically updates the DOM when the state changes, demonstrating the concept of reactivity in JavaScript.Validate Input
const user = { name: '', age: 0 }; const handler = { set(target, property, value) { if (property === 'name') { if (typeof value !== 'string' || value.trim() === '') { console.log('Invalid name. It must be a non-empty string.'); return false; } } if (property === 'age') { if (typeof value !== 'number' || value <= 0) { console.log('Invalid age. It must be a positive number.'); return false; } } target[property] = value; return true; } }; const proxyUser = new Proxy(user, handler); // Valid input proxyUser.name = 'Alice'; // Sets the name to 'Alice' proxyUser.age = 30; // Sets the age to 30 // Invalid input proxyUser.name = ''; // Logs: Invalid name. It must be a non-empty string. proxyUser.age = -5; // Logs: Invalid age. It must be a positive number. console.log(proxyUser); // { name: 'Alice', age: 30 }
The
set
trap in the handler performs the validation and controls whether the property assignment is allowedAuditing and Logging
const data = { username: 'john_doe', email: 'john@example.com' }; const handler = { deleteProperty(target, property) { console.log(`Property '${property}' was deleted`); return delete target[property]; } }; const proxyData = new Proxy(data, handler); delete proxyData.email; // Logs: Property 'email' was deleted
The code uses a proxy to intercept and log deletions of properties from the data object. When
proxyData.email
is deleted, it logs the deletion and then removes the property.Protect Sensitive Data
const user = { id: 1, username: 'admin', role: 'superuser' }; const handler = { deleteProperty(target, property) { if (property === 'role') { console.log(`Cannot delete critical property: ${property}`); return false; } return delete target[property]; } }; const proxyUser = new Proxy(user, handler); delete proxyUser.role; // Cannot delete critical property: role delete proxyUser.username; // Property is deleted normally
The code uses a proxy to prevent the deletion of the
role
property from theuser
object, logging a message and returningfalse
if attempted. Other properties, likeusername
, can be deleted normally.Garbage Collection and Memory Management
const resources = { file1: 'content1', file2: 'content2' }; const handler = { deleteProperty(target, property) { console.log(`Releasing resources associated with ${property}`); // Perform cleanup tasks return delete target[property]; } }; const proxyResources = new Proxy(resources, handler); delete proxyResources.file1; // Logs: Releasing resources associated with file1
In this example, a
Proxy
is created around theresources
object to intercept the deletion of properties. When a property is deleted, thedeleteProperty
trap logs a message and can perform cleanup tasks before removing the specified property from the target object.Soft Deletion
const product = { id: 101, name: 'Smartphone', isDeleted: false }; const handler = { deleteProperty(target, property) { if (property === 'name') { console.log('Soft deleting the product.'); target.isDeleted = true; return true; // Indicate success } return delete target[property]; } }; const proxyProduct = new Proxy(product, handler); delete proxyProduct.name; // Also Logs: Soft deleting the product. console.log(product.isDeleted); // true
In this example, when attempting to delete the
name
property from theproduct
object via the proxy, it triggers a soft delete by settingisDeleted
totrue
while logging a message instead of actually removing the property.Manage Defaults and Missing properties
const defaultValues = { name: 'Unknown', age: 0, country: 'Unknown' }; const handler = { get(target, prop) { // If the property doesn't exist, return a default value return prop in target ? target[prop] : defaultValues[prop]; } }; const person = { name: 'John', country: 'USA' }; const proxyPerson = new Proxy(person, handler); console.log(proxyPerson.name); // John console.log(proxyPerson.age); // 0 (default) console.log(proxyPerson.country); // USA
In this example, the proxy intercepts property access on the
person
object, returning the actual value for existing properties likename
andcountry
. For properties not found in theperson
object, such asage
, it provides a default value from thedefaultValues
object, which is0
.Rate Limiter
const rateLimiter = (fn, limit) => { let calls = 0; return new Proxy(fn, { apply(target, thisArg, argumentsList) { if (calls >= limit) { console.log('Rate limit exceeded. Try again later.'); return; } calls++; return target.apply(thisArg, argumentsList); } }); }; const apiCall = () => { console.log('API call made'); }; const limitedApiCall = rateLimiter(apiCall, 3); // Simulate multiple API calls limitedApiCall(); // API call made limitedApiCall(); // API call made limitedApiCall(); // API call made limitedApiCall(); // Rate limit exceeded. Try again later.
This Proxy-based rate limiter intercepts function calls and restricts them to a specified limit. If the limit is exceeded, it blocks further calls and displays a message instead of executing the function.
Profiling - Function Execution Time
function expensiveOperation() { for (let i = 0; i < 1e6; i++) { } // Simulate a costly operation return "Operation Complete"; } const handler = { apply(target, thisArg, args) { console.time('Execution Time'); const result = target.apply(thisArg, args); console.timeEnd('Execution Time'); return result; } }; const proxyOperation = new Proxy(expensiveOperation, handler); console.log(proxyOperation()); // Logs execution time and result
The example above shows how to profile function execution by measuring how long a function takes to execute using a Proxy - the
apply
trap intercepts the function call and allows us to measure how long theexpensiveOperation
function takes to execute usingconsole.time
andconsole.timeEnd
Access Nested Objects in a cleaner way
const user = { name: 'John Doe', address: { street: '123 Main St', city: 'New York', zip: '10001', }, }; const fullAddressHandler = { get(target, prop) { if (prop === 'address') { const { street, city, zip } = target.address; return `${street}, ${city}, ${zip}`; } return target[prop]; }, }; const proxiedUser = new Proxy(user, fullAddressHandler); console.log(proxiedUser.address); // 123 Main St, New York, 10001
The
address
property itself has been overridden via theProxy
to return the full address string instead of the nested object. This allows you to access the full address usingproxiedUser.address
directly
Performance Issues with Proxy
Using proxies can introduce performance overhead due to the additional layer of indirection and the traps that are executed for each intercepted operation. Here are some common performance issues associated with proxies:
Overhead of Traps: Each operation (e.g., get, set, delete) on the proxy object triggers the corresponding trap in the handler. This can add significant overhead, especially if the operations are frequent.
Complexity of Traps: If the logic inside the traps is complex or involves heavy computations, it can further degrade performance.
Lack of Optimization: JavaScript engines are highly optimized for standard object operations. Proxies, being a relatively newer feature, may not benefit from the same level of optimization.
Memory Usage: Proxies can increase memory usage due to the additional objects and closures created for the handler and traps.
Benchmarks Show Slower Operations: Benchmarks across engines have shown that Proxies can be multiple times slower than regular object operations. This slowdown varies depending on the type of trap being used, the complexity of the handler logic, and the specific use case (e.g., simple property access vs. function invocation).
Mitigating Performance Concerns
Use Proxies Selectively: Only apply Proxies where necessary, rather than wrapping every object in a Proxy.
Minimize Complex Logic in Traps: Avoid performing heavy computations inside traps to reduce performance impact.
Combine Proxy and Traditional Object Patterns: For cases where performance is critical, you can mix Proxies with other methods like getters/setters or class-based models, using Proxies only when absolutely necessary.
Test for Bottlenecks: Always benchmark your code to identify where Proxies may be causing performance issues and evaluate whether the trade-offs are worth it.
Conclusion
In conclusion, the JavaScript Proxy object opens up a world of possibilities for developers by allowing fine-grained control over object behavior. From managing reactivity and caching to providing default values and implementing complex access patterns, Proxies enable cleaner, more efficient code with minimal efforts.
Much like a shape-shifter, the Proxy can seamlessly adapt to the ever-changing needs of your application, offering both flexibility and control without altering the core structure of your objects.
As you explore their capabilities, you may find that Proxies not only simplify your existing code but also inspire innovative solutions to common development challenges. Whether you're enhancing performance, enforcing validation, or simply making your code more expressive, the Proxy API is a powerful tool that can elevate your JavaScript projects to new heights. Embrace the flexibility and potential of this shape shifter, and you may discover new ways to think about and structure your applications.
Now that you've explored the versatility and power of the JavaScript Proxy API, it's time to put it into practice!
Try experimenting with Proxies in your own projects. Have an interesting use case or experience with Proxies? Share your thoughts and use cases in the comments—let’s learn together!
Subscribe to my newsletter
Read articles from Harpreet Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Harpreet Singh
Harpreet Singh
I am a developer from India. Interested in learning and sharing the knowledge.