Skip to main content

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:

  1. Target: The original object you want to wrap
  2. Handler: An object that defines "traps" (methods that intercept operations)
javascript
const proxy = new Proxy(target, handler);

Let's start with a simple example:

javascript
// 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.

javascript
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.

javascript
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.

javascript
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.

javascript
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

javascript
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.

javascript
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.

javascript
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

javascript
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

  1. Create a Proxy that prevents deletion of certain "protected" properties.
  2. Implement a case-insensitive object where obj.name and obj.NAME refer to the same property.
  3. Create a "default value" proxy that returns a specified default value for any property that doesn't exist.
  4. Build a proxy that provides "computed properties" that are calculated on demand.
  5. 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! :)