Combine Proxy traps with the Reflect API
Written byPhuoc Nguyen
Created
07 Jan, 2024
Tags
JavaScript Proxy, Reflect API
Throughout this series, we've discovered that the Proxy handler offers numerous traps that allow us to alter the default behaviors of object methods and operations.
Take a look at the
`get`
and `set`
traps as an example:js
const handler = {
get(target, property) {
return target[property];
},
set(target, property, value) {
target[property] = value;
return true;
},
};
const proxy = new Proxy(target, handler);
The traps work well when the target is a normal
`Object`
, but there are some cases where they don't function properly. In this post, we will explore some examples that illustrate the problem and show how to fix it using the Reflect API.#Creating a Proxy for a Map object
In this section, we'll explore an issue that arises when creating a proxy for an object with built-in
`get`
and `set`
methods, such as a `Map`
. Take a look at the code snippet below:js
const map = new Map([
['name', 'John Smith'],
['age', 42],
]);
const proxyMap = new Proxy(map, {});
The code above creates a
`Map`
object and sets it up with two key-value pairs: `'name'`
with value `'John Smith'`
, and `'age'`
with value `42`
. Then, a `proxyMap`
is created using the `Proxy`
constructor with the `map`
object as its first argument and an empty handler object as its second argument.Now, let's try setting and getting a property from our proxied map:
js
// Uncaught TypeError: Method Map.prototype.get called on incompatible receiver
proxyMap.get('name');
// Uncaught TypeError: Method Map.prototype.set called on incompatible receiver
proxyMap.set('age', 30);
When we attempted to retrieve and assign values from our proxied
`Map`
, we encountered errors. This occurred because the proxy handler lacked any traps for these operations, causing the `Map`
object's default behavior to be used instead. However, since `proxyMap`
is not a `Map`
instance, calling methods such as `get`
and `set`
will result in a type error.To resolve this issue, we will establish a
`get`
handler.js
const handler = {
get(target, property) {
// ...
},
};
const proxyMap = new Proxy(target, handler);
When we call the
`get`
and `set`
functions in our proxied Map, here's what goes down:Invocation | `target` | `property` |
---|---|---|
`proxyMap.get(...)` | The original map | `get` |
`proxyMap.set(...)` | The original map | `set` |
`proxyMap.name` | The original map | `name` |
`proxyMap.age` | The original map | `age` |
As you can see, the
`property`
parameter in the `get`
handler can be either an existing property of the map or the name of a function that we want to execute. The easiest way to implement the `get`
handler is to check whether the `property`
parameter is one of the supported functions provided by our Map.js
const handler = {
get(target, property) {
if (propery === "set") {
return Map.prototype.set.bind(target);
}
if (property === "get") {
return Map.prototype.get.bind(target);
}
return target.get(property);
},
};
The
`get`
handler is like a trap that intercepts property access on an object. When you define the `get`
handler in a Proxy, it runs every time you access a property on the object being proxied.In our example, when we access a property of our proxied
`Map`
, the `get`
handler checks if the property is one of the supported functions provided by our Map (like `set`
or `get`
). If it is, it returns that function bound to the original target object. Otherwise, it returns the value of that property on the target object.This approach ensures that all operations on our proxied object will be handled correctly while still allowing us to modify or extend its behavior as needed.
However, for larger objects with many methods or properties, checking if the property is one of the existing methods can be tedious and error-prone. So, while checking for supported functions works well for small objects like
`Map`
, it's not scalable for larger ones.#Proxy a class with private fields
In our previous post, we discussed how to use Proxy to create private properties for a class. However, modern JavaScript now natively supports private fields by simply prefixing them with the # symbol.
js
const handler = {};
class ProtectedPerson {
#ssn = "";
constructor(name, age, ssn) {
this.name = name;
this.age = age;
this.#ssn = ssn;
return new Proxy(this, handler);
}
getSsn() {
return this.#ssn;
}
}
In this example, we introduced the
`ProtectedPerson`
class which has three properties: `name`
, `age`
, and `#ssn`
. The first two are public properties that can be accessed from outside the class, while the third is a private field that can only be accessed from within the class.To create a new instance of the class, we use the constructor function which takes three arguments:
`name`
, `age`
, and `ssn`
. These values are assigned to their respective properties when a new instance of the class is created.We also added a
`Proxy`
inside the constructor function to intercept property access and modify its behavior. This allows us to control how the properties are accessed and manipulated.The
`ProtectedPerson`
class also has a method called `getSsn()`
which returns the value of the private field `#ssn`
. This method can be called from outside of the class to retrieve the value of the private field.To create a new instance of
`ProtectedPerson`
, all we need to do is call the constructor function with the required arguments. For example, to create a new person object with the name "John Smith", age 42, and social security number "123-45-6789", we can simply write:js
const person = new ProtectedPerson("John Smith", 42, "123-45-6789");
However, if we attempt to call the
`getSsn()`
method from the person instance, an error will occur.js
// Uncaught TypeError: Cannot read private member #ssn
// from an object whose class did not declare it
console.log(person.getSsn());
The
`getSsn()`
function is only available for the `ProtectedPerson`
class and not for its proxied version because it accesses a private field named `#ssn`
. This field is only accessible within the class itself. When we create a proxy of an object, we can intercept and modify its method calls, but we cannot access its private fields from outside of the original class. Therefore, any attempt to call `getSsn()`
on a proxied instance of `ProtectedPerson`
will result in an error.Luckily, there's a simpler way to handle this situation using the Reflect API. The Reflect API comes with a set of default handlers that can be used to proxy any object without requiring custom traps for each method or operation. In the next section, we'll explore how to use the Reflect API to solve the issue.
#Introducing the Reflect API
The Reflect API is a powerful built-in JavaScript tool that allows developers to intercept and modify object operations. This API was introduced in ECMAScript 6 to provide a unified interface for working with objects and their properties.
Some of the most common Reflect APIs include:
`Reflect.get(target, property, receiver)`
: This method returns the value of a specified property on the target object. You can also specify an optional`receiver`
parameter to specify the object to use as`this`
when getting the property value.
Here's an example to help illustrate how it works:
js
const person = {
name: "John Smith",
age: 42,
};
const name = Reflect.get(person, "name");
console.log(name); // `John Smith`
In this example, we're using
`Reflect.get()`
to get the value of the `name`
property from the `person`
object.`Reflect.set(target, property, value, receiver)`
is a method that sets the value of a specific property on the target object to a given value. You can also specify the`receiver`
parameter, which is optional and determines the object that should act as`this`
when setting the property value.
Here's an example to help illustrate how it works:
js
const person = {
name: "John Smith",
age: 30,
};
Reflect.set(person, "age", 42);
console.log(person.age); // 42
In this example, we're using
`Reflect.set()`
to change the value of the `age`
property on the `person`
object to 42.Reflect offers not only those APIs, but also a range of additional functions, including:
`Reflect.has(target, property)`
: This function returns a true or false value, indicating whether or not the target object has a property with the specified name.`Reflect.deleteProperty(target, property)`
: Use this function to remove a property from the target object with the specified name.`Reflect.construct(constructor, args[, newTarget])`
: This function creates a new instance of a constructor function using an array of arguments. You can use the optional`newTarget`
parameter to specify a different constructor function to use for creating the instance.`Reflect.defineProperty(target, propertyKey, attributes)`
: This function defines a new property on an object with the given name and attributes.`Reflect.getOwnPropertyDescriptor(target, propertyKey)`
: Use this function to return an object describing a named own or inherited properties corresponding to those found in`Object.getOwnPropertyDescriptor()`
.
#Using Reflect with Proxy
It's important to note that the Reflect API and proxy traps are quite similar. They both provide a way to intercept and modify operations on objects in JavaScript.
For instance, when we use
`Reflect.get()`
to retrieve the value of a property from an object, it's like defining a `get`
trap on a proxy object. Similarly, when we use `Reflect.set()`
to set the value of a property on an object, it's like defining a `set`
trap on a proxy object.The main difference between using the Reflect API and defining traps directly on a proxy object is that the Reflect API provides default behavior for each operation. This means that if we don't define custom behavior for an operation, the default behavior will be used instead. On the other hand, when we define traps directly on a proxy object, we must provide custom behavior for every operation we want to intercept or modify.
Overall, both the Reflect API and proxy traps are powerful features of modern JavaScript. They allow us to create more flexible and customizable objects.
To address the issue mentioned earlier, we'll be using the Reflect API. Specifically, we'll need to make some modifications to the
`get`
and `set`
trap handlers.js
const handler = {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
return typeof value == 'function' ? value.bind(target) : value;
},
set(target, property, value, receiver) {
return Reflect.set(target, property, value, receiver);
},
};
In our handler, we use Reflect APIs to provide a default implementation for each trap. For example, in the
`get`
trap, we retrieve the value of the property using `Reflect.get()`
. If the value is a function, we bind it to the original target object using `bind()`
. This way, we can ensure that all operations on our proxied object will be handled correctly while still allowing us to modify or extend its behavior as needed.Similarly, in the
`set`
trap, we call `Reflect.set()`
to set the value of the property on the target object. By using `Reflect`
methods instead of directly accessing properties and methods on the target object, we can ensure that our proxy works correctly with any type of object and does not interfere with its normal behavior.Let's create a Proxy of a Map using the new version of the handler.
js
const map = new Map([
['name', 'John Smith'],
['age', 42],
]);
const proxyMap = new Proxy(map, handler);
Now, we can use the functions provided by the original Map to manipulate its properties. For example, we can easily retrieve the value of any property:
js
proxyMap.get('name'); // `John Smith`
proxyMap.get('age'); // 42
Please update the property value.
js
proxyMap.set('age', 30);
proxyMap.get('age'); // 30
As an example of accessing a private field within a class, the
`getSsn()`
function now returns the value of the private field `#ssn`
without encountering any issues.js
const person = new ProtectedPerson('John Smith', 42, '123-45-6789');
person.getSsn(); // `123-45-6789`
#Conclusion
To sum up, using the Reflect API in our proxy traps handler gives us a more flexible and extensible way to intercept and modify operations on objects. By relying on the default behavior provided by the Reflect API, we can reduce the amount of custom code required to create a robust and reliable proxy object.
Moreover, by using the
`Reflect`
methods instead of directly accessing properties and methods on the target object, we can ensure that our proxy works correctly with any type of object and does not interfere with its normal behavior.All in all, the combination of Proxy traps and the Reflect API provides a powerful tool for creating highly customizable and flexible objects in JavaScript.
Questions? 🙋
Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Recent posts ⚡
Newsletter 🔔
If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn