Understanding Proxies in JavaScript
An overview of what a Proxy is, how it works, and why it exists.
If you're looking to dive in to some more advanced JavaScript concepts, the Proxy API is a great place to start! Proxies give you the ability to control how properties are accessed on objects, including getting, setting, and deleting values. In addition, you can use proxies to implement custom behavior for common operations like iteration and assignment.
Proxies are a relatively new feature in JavaScript, and provides a way to create "virtual" objects that can intercept property accesses and perform custom operations.
What are Proxies?
A Proxy is like a middle-man who can sit between you and the thing you're trying to access. You can tell the Proxy what you want to do, and it can either do it for you, stop you from doing it, or do something else entirely.
In JavaScript, objects can have properties that are themselves objects. For example, you might have an object with a property named "foo" that contains another object:
var obj = {
foo: {
// ...
}
};
Proxies provide a way to intercept these property accesses and perform custom operations. In the above example, we could use a proxy to intercept calls to obj.foo
and do something else instead:
const proxy = new Proxy(obj, {
get: function(target, property, receiver) {
if (property == "foo") {
// ... do something else
} else {
// return the default behavior
return Reflect.get(target, property, receiver);
}
}
});
In this case, we intercept calls to get
(which is used to retrieve a property value) and check if the property name is "foo". If it is, we do something else; otherwise, we return the default behavior.
Creating Proxies
Proxies are created using the new Proxy()
constructor:
const proxy = new Proxy(target, handler);
The target
argument is the object that the proxy will "proxy" for; that is, operations on the proxy will be forwarded to the target
object. The handler
argument is an object that defines which operations the proxy will intercept.
Proxy Handler Methods
The handler
object can define any of the following methods:
get(target, property, receiver)
: Called when a property is accessed.set(target, property, value, receiver)
: Called when a property is set.has(target, property)
: Called when using thein
operator.deleteProperty(target, property)
: Called when using thedelete
operator.apply(target, thisArg, argumentsList)
: Called when a function is invoked.construct(target, argumentsList, newTarget)
: Called when a constructor is invoked.
These methods all have the same signature as the corresponding Reflect
methods. The Reflect
methods are used to perform the default behavior for an operation; if you want to customize the behavior, you can override the corresponding handler method.
Example: Validating Property Values
In these examples, the JavaScript Proxy API acts like a security guard at a nightclub. It can intercept requests and make sure they are valid before allowing them to pass through.
Let's say we have an object that represents a user's profile, and we want to make sure that the age
property is always a positive number. We can do this by creating a proxy for the object, and intercepting calls to set
:
const profile = {
name: "John Doe",
age: 42
};
const proxy = new Proxy(profile, {
set: function(target, property, value, receiver) {
if (property == "age") {
if (typeof value != "number" || value < 0) {
throw new Error("Invalid age");
}
}
// return the default behavior
return Reflect.set(target, property, value, receiver);
}
});
Now, if we try to set the age
property to an invalid value, we'll get an error:
proxy.age = "42"; // Error: Invalid age
Example: Logging Property Accesses
In this example, we'll create a proxy that logs all property accesses:
const obj = {
foo: "bar"
};
const proxy = new Proxy(obj, {
get: function(target, property, receiver) {
console.log("getting property " + property);
// return the default behavior
return Reflect.get(target, property, receiver);
},
set: function(target, property, value, receiver) {
console.log("setting property " + property + " to " + value);
// return the default behavior
return Reflect.set(target, property, value, receiver);
}
});
Now, whenever we access or set a property on proxy
, we'll see a message in the console:
proxy.foo; // getting property foo
proxy.foo = "baz"; // setting property foo to baz
Proxy Revocability
Proxies are revocable, which means that they can be disabled after they've been created. To do this, we use the Proxy.revocable()
method:
const obj = {
foo: "bar"
};
const revocable = Proxy.revocable(obj, {
get: function(target, property, receiver) {
console.log("getting property " + property);
// return the default behavior
return Reflect.get(target, property, receiver);
},
});
const proxy = revocable.proxy;
Now, proxy
behaves just like our previous example. We can access and set properties, and we'll see messages in the console:
proxy.foo; // getting property foo
proxy.foo = "baz"; // setting property foo to baz
When we're finished with the proxy, we can disable it by calling the revoke()
method:
revocable.revoke();
Now, if we try to access or set properties on proxy
, we'll get an error:
proxy.foo; // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.foo = "baz"; // TypeError: Cannot perform 'set' on a proxy that has been revoked
Conclusion
Proxies provide a powerful way to intercept and customize property accesses in JavaScript. I hope this has helped you understand how to create proxies, and how to use them to perform operations such as validating property values and logging property accesses.
Related to Proxies is the incredibly useful Reflect API, so stay follow or signup for an article on that soon.
Let me know in the comments if you liked this or have any questions—be sure to follow me for more like this!