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:
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:
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:
// 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.
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.
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.
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:
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.
// 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 optionalreceiver
parameter to specify the object to use asthis
when getting the property value.
Here's an example to help illustrate how it works:
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 thereceiver
parameter, which is optional and determines the object that should act asthis
when setting the property value.
Here's an example to help illustrate how it works:
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 optionalnewTarget
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 inObject.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.
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.
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:
proxyMap.get('name'); // `John Smith`
proxyMap.get('age'); // 42
Please update the property value.
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.
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.
If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks π. Your support would mean a lot to me!
If you want more helpful content like this, feel free to follow me:
Top comments (0)