JavaScript Proxies
Introduction
JavaScript Proxies, introduced in ES6 (ECMAScript 2015), provide a powerful way to intercept and customize operations performed on objects. They allow you to create a wrapper around an object that can intercept and redefine fundamental operations like property lookup, assignment, enumeration, function invocation, and more.
Think of a Proxy as a middleman or an intermediary that stands between your code and an object. Instead of interacting directly with the object, you interact with the Proxy, which can modify how the operations work.
Basic Syntax
The syntax for creating a Proxy involves two main components:
- Target: The original object you want to wrap
- Handler: An object that defines "traps" (methods that intercept operations)
const proxy = new Proxy(target, handler);
Let's start with a simple example:
// The target object
const target = {
message: "Hello, world!"
};
// The handler object with a get trap
const handler = {
get: function(target, prop, receiver) {
console.log(`Property '${prop}' has been accessed`);
return target[prop];
}
};
// Create a proxy
const proxy = new Proxy(target, handler);
// Using the proxy
console.log(proxy.message);
Output:
Property 'message' has been accessed
Hello, world!
In this example, whenever we try to access a property on our proxy, the get
trap is triggered, logging a message before returning the actual value.
Common Proxy Traps
Proxies support various traps that can intercept different operations. Here are some of the most commonly used traps:
1. The get
Trap
The get
trap is triggered when a property is accessed.
const numbers = [1, 2, 3];
const arrayProxy = new Proxy(numbers, {
get: function(target, prop) {
// Check if the property is a number (array index)
if (!isNaN(prop)) {
console.log(`Accessing element at index ${prop}`);
}
return target[prop];
}
});
console.log(arrayProxy[1]);
console.log(arrayProxy.length);
Output:
Accessing element at index 1
2
3
2. The set
Trap
The set
trap is triggered when a property is set.
const user = {
name: "Alice",
age: 25
};
const userProxy = new Proxy(user, {
set: function(target, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (prop === 'age' && value < 0) {
throw new RangeError('Age must be positive');
}
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
return true; // Indicates success
}
});
userProxy.name = "Bob";
try {
userProxy.age = "twenty-six"; // This will throw an error
} catch (e) {
console.error(e.message);
}
console.log(user); // Original object is modified
Output:
Setting name to Bob
Age must be a number
{ name: 'Bob', age: 25 }
3. The apply
Trap
The apply
trap intercepts function calls.
function greet(name) {
return `Hello, ${name}!`;
}
const greetProxy = new Proxy(greet, {
apply: function(target, thisArg, args) {
console.log(`Function was called with arguments: ${args}`);
// Make name uppercase before passing to the original function
const result = target.apply(thisArg, [args[0].toUpperCase()]);
return result;
}
});
console.log(greetProxy("world"));
Output:
Function was called with arguments: world
Hello, WORLD!
4. The has
Trap
The has
trap intercepts the in
operator.
const secretData = {
publicField: "This is public",
_privateField: "This should be hidden"
};
const secretProxy = new Proxy(secretData, {
has: function(target, prop) {
// Hide properties that start with an underscore
if (prop.startsWith('_')) {
return false;
}
return prop in target;
}
});
console.log('publicField' in secretProxy);
console.log('_privateField' in secretProxy);
console.log(secretProxy._privateField); // Note: `has` doesn't affect direct access
Output:
true
false
This should be hidden
Real-World Applications of Proxies
Let's explore some practical use cases for JavaScript Proxies:
1. Validation and Type Checking
function createValidatedObject(validations) {
return new Proxy({}, {
set(target, prop, value) {
if (validations.hasOwnProperty(prop)) {
const validator = validations[prop];
if (validator(value)) {
target[prop] = value;
return true;
} else {
throw new Error(`Invalid value for property ${prop}`);
}
} else {
// No validation specified, allow the operation
target[prop] = value;
return true;
}
}
});
}
const person = createValidatedObject({
age: value => typeof value === 'number' && value > 0,
name: value => typeof value === 'string' && value.length > 0
});
person.name = "Alice"; // Valid
console.log(person.name);
try {
person.age = -5; // Invalid
} catch (e) {
console.error(e.message);
}
Output:
Alice
Invalid value for property age
2. Creating a Revocable Access
Sometimes you want to provide temporary access to an object and then revoke it.
const user = {
name: "Alice",
role: "admin"
};
const { proxy, revoke } = Proxy.revocable(user, {
get: function(target, prop) {
console.log(`Accessing ${prop}`);
return target[prop];
}
});
console.log(proxy.name); // Works fine
// Later, revoke access
revoke();
try {
console.log(proxy.name); // Will throw TypeError
} catch (e) {
console.error("Error:", e.message);
}
Output:
Accessing name
Alice
Error: Cannot perform 'get' on a proxy that has been revoked
3. Logging and Debugging
Proxies are great for adding logging capabilities without modifying the original code.
function createLoggingProxy(target, name = "Object") {
return new Proxy(target, {
get(target, prop) {
const value = target[prop];
console.log(`${name}.${prop} accessed, value: ${value}`);
return value;
},
set(target, prop, value) {
console.log(`${name}.${prop} changed from ${target[prop]} to ${value}`);
target[prop] = value;
return true;
}
});
}
const user = createLoggingProxy({ name: "Alice", age: 25 }, "user");
const oldAge = user.age;
user.age = 26;
Output:
user.age accessed, value: 25
user.age changed from 25 to 26
4. Creating a "Missing Property" Handler
const dictionary = {
hello: "greeting used when meeting someone",
goodbye: "greeting used when parting"
};
const safeDict = new Proxy(dictionary, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return `Word "${prop}" not found in dictionary`;
}
}
});
console.log(safeDict.hello);
console.log(safeDict.hello123);
Output:
greeting used when meeting someone
Word "hello123" not found in dictionary
Summary
JavaScript Proxies provide a powerful way to intercept and customize operations on objects. They allow you to:
- Validate data before it's assigned to properties
- Log operations for debugging purposes
- Create virtual properties
- Implement access control
- Extend built-in objects safely
- Create "smart" objects that respond dynamically to operations
Although powerful, Proxies should be used judiciously as they can affect performance and make code harder to understand if overused.
Additional Resources
To learn more about JavaScript Proxies, check out these resources:
Exercises
- Create a Proxy that prevents deletion of certain "protected" properties.
- Implement a case-insensitive object where
obj.name
andobj.NAME
refer to the same property. - Create a "default value" proxy that returns a specified default value for any property that doesn't exist.
- Build a proxy that provides "computed properties" that are calculated on demand.
- Implement a history tracking system that records all changes made to an object.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)