DEV Community

Cover image for Combine Proxy traps with the Reflect API
Phuoc Nguyen
Phuoc Nguyen

Posted on • Originally published at phuoc.ng

Combine Proxy traps with the 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:

const handler = {
    get(target, property) {
        return target[property];
    },
    set(target, property, value) {
        target[property] = value;
        return true;
    },
};

const proxy = new Proxy(target, handler);
Enter fullscreen mode Exit fullscreen mode

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, {});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
    },
};
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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:

const person = {
    name: "John Smith",
    age: 42,
};

const name = Reflect.get(person, "name");
console.log(name);  // `John Smith`
Enter fullscreen mode Exit fullscreen mode

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:

const person = {
    name: "John Smith",
    age: 30,
};

Reflect.set(person, "age", 42);
console.log(person.age);    // 42
Enter fullscreen mode Exit fullscreen mode

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.

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);
    },
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Please update the property value.

proxyMap.set('age', 30);
proxyMap.get('age');    // 30
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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)