Exploring JavaScript - Proxy and Reflect

Table of Contents

While studying decorators in TypeScript, I came across references to Proxy, and I decided to organize my thoughts on this topic that I had wanted to clarify for some time.

1. Basics of Proxy

1.1. Declaring a Proxy

A Proxy is an object that wraps another object, intercepting operations performed on it to either handle them or to perform additional tasks. After the additional tasks, it may pass the operations to the original object.

A Proxy object is created in the following manner:

let proxy = new Proxy(target, handler);

target is the object that the proxy wraps, which can be any JS object. handler is an object that defines the operations the proxy will intercept and how to handle those operations. The methods in the handler that intercept the object's actions are called traps.

When an operation is invoked on the created proxy object, if there is a trap corresponding to the operation in the handler, that trap is executed; otherwise, the proxy forwards the operation to the original object.

In the following case, since there are no traps defined in the handler, all operations applied to proxy are forwarded to target. Unlike a regular object, the proxy does not have properties.

let target = {};
let proxy = new Proxy(target, {});

1.2. Types of Traps

The operations that can be intercepted using traps in a Proxy include the following. These correspond to the internal methods of the original object, and can be intercepted through the proxy's traps.

The following table is sourced from the article Proxy and Reflect.

Trap NameCorresponding Internal MethodInvocation Time
get[[Get]]When reading a property
set[[Set]]When writing a property
has[[HasProperty]]When using the in operator
deleteProperty[[Delete]]When using the delete operator
apply[[Call]]When invoking a function
construct[[Construct]]When using the new operator
getPrototypeOf[[GetPrototypeOf]]Object.getPrototypeOf
setPrototypeOf[[SetPrototypeOf]]Object.setPrototypeOf
isExtensible[[IsExtensible]]Object.isExtensible
preventExtensions[[PreventExtensions]]Object.preventExtensions
defineProperty[[DefineOwnProperty]]Object.defineProperty, Object.defineProperties
getOwnPropertyDescriptor[[GetOwnProperty]]Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
ownKeys[[OwnPropertyKeys]]Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries

1.2.1. Rules for Using Traps

When using traps, the following rules must be adhered to:

If the operation for writing a value is successful, [[Set]] must return true; otherwise, it should return false.

If the operation for deleting a value is successful, [[Delete]] must return true; otherwise, it should return false.

When [[GetPrototypeOf]] is applied to the proxy object, it should return the same value as applying [[GetPrototypeOf]] to the target object. Naturally, their prototypes should be the same.

Other rules can be found in the specifications for the internal methods of Proxy.

2. Examples of Trap Usage

2.1. get Trap

The get trap executes when a property is read. It is defined in the form get(target, property, receiver).

target is the object to which the operation will be directed, property is the property name, and receiver refers to the proxy object or any object inheriting from the proxy, which serves as the this context when the getter is invoked. The receiver can initially be omitted.

Let us print a message when the specified key does not exist in the object and return the key itself.

let target = {};
let proxy = new Proxy(target, {
  get(target, property, receiver) {
    if (property in target) {
      return target[property];
    } else {
      console.log("no such property in the given target!");
      return property;
    }
  }
});

target[1] = "A";
// A
console.log(proxy[1]);
// no such ...
// 2
console.log(proxy[2]);

2.2. set Trap

The set trap is invoked when attempting to write a value to a property. It is defined as set(target, property, value, receiver).

Naturally, target is the object to which the operation will be directed, property is the property name, value is the value to write to the property, and receiver is the same as in the get trap.

To ensure only numbers can be added to an array, the implementation can be as follows:

let target = [];

let proxy = new Proxy(target, {
  set(target, property, value) {
    if (typeof value === "number") {
      console.log(value, "is added to the array!");
      target[property] = value;
      return true;
    } else {
      console.log("only number can be added to the array!");
      return false;
    }
  }
});

Methods like push, which internally use [[Set]], will also function correctly with this proxy.

There is a rule to follow when using the set trap: if the operation for writing a value is successful, then [[Set]] must return true; otherwise, it must return false. Returning a falsy value will result in a TypeError.

2.3. has Trap

The has trap is invoked when the in operator is used. It is defined as has(target, property).

You can perform specific validations for a property. For instance, the following code validates whether a value falls within a specified range when the in operator is invoked.

let range = {
  start: 1,
  end: 10
};

range = new Proxy(range, {
  has(target, property) {
    return target.start <= property && property <= target.end;
  }
});

console.log(5 in range); // true

You can find further examples of trap usage on various reference pages.

Limitations of Proxy

While proxies allow intercepting the operations of existing objects to perform additional tasks, they also have limitations. Proxies operate by intercepting the internal methods of objects, but some objects function differently through their own internal methods.

For instance, the Map object stores data in a special slot called [[MapData]], and thus the proxy cannot effectively handle it.

3. Reflect

3.1. Basics of Reflect

Reflect offers a way to directly use internal methods similar to Proxy. However, it does not create new objects but allows us to use the internal methods of existing objects. It is neither a constructor function nor a class, so instances cannot be created, nor can it be invoked with new.

The methods of Reflect are identical to those provided in the handlers of Proxy. The first argument is the target on which to apply the internal methods, and the remaining arguments are aligned with the handlers of Proxy.

For example, Reflect.get allows using the [[Get]] internal method:

const obj = {
  foo: 1,
  bar: 2,
};

console.log(Reflect.get(obj, "foo")); // 1

Of course, it can also be used alongside Proxy:

const obj = {
  foo: 1,
  bar: 2,
};

const proxy = new Proxy(obj, {
  get(target, property) {
    console.log("get is called!");
    return Reflect.get(target, property);
  }
});

Call operators like new and delete can be invoked similarly through Reflect.construct and Reflect.deleteProperty.

However, these functionalities can be achieved without Reflect. For instance, accessing a property directly with obj.foo works without invoking Reflect. Therefore, let’s understand the advantages of using Reflect.

3.2. Advantages of Reflect

Consider an object that handles the name property. Suppose we fetch this property through a proxy object.

let user = {
  _name: "김성현",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, property) {
    return target[property];
  }
});

console.log(userProxy.name); // 김성현

Once userProxy is created, it makes sense to use it instead of user. However, what happens if an object inherits from userProxy?

let userOnline = {
  __proto__: userProxy,
  _name: "마녀",
};

// It seems that "마녀" should be returned, but "김성현" is shown.
console.log(userOnline.name);

Since userOnline does not have a name property, it resorts to the prototype userProxy, which returns user's name due to its get trap being defined as target[property].

Using Reflect helps solve this issue. By adjusting the get trap of userProxy to use Reflect, the receiver retains a proper reference to this, allowing Reflect.get to return userOnline's name property correctly.

let user = {
  _name: "김성현",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, property, receiver) {
    // Can also use return Reflect.get(...arguments)
    return Reflect.get(target, property, receiver);
  }
});

let userOnline = {
  __proto__: userProxy,
  _name: "마녀",
};
// Outputs "마녀"
console.log(userOnline.name);

References

Modern JS Tutorial, Proxy and Reflect: https://ko.javascript.info/proxy

JavaScript Proxy. But now with Reflect added: https://ui.toast.com/posts/ko_20210413

JavaScript Proxy: https://yceffort.kr/2021/03/javascript-proxy