Let’s learn Proxy and Reflect
Everyone is aware that the Ozone layer surrounds the earth by intercepting direct incoming energy or objects into the earth. It’s a protective layer for everything on this planet. Having that in mind let’s simply walk through what is a Proxy and what is Reflect in JavaScript.
Proxy
In general terms, Proxy is a pattern where it acts as a middle layer between two parties intercepting the communication between them. In JavaScript, Proxy is an object wrapper for another object intercepting the read, write and other object functions.
First, learn the terms that you are going to see!!
Proxy — A wrapper/layer for another object.
Target — Any object, array, function or even a proxy itself.
Trap — Methods that intercept original operations of the target object, if provided any
Handler — An object having traps or configurations. If this is empty, operations will directly happen on the target object as if there was no proxy.
A proxy accepts two parameters namely target and handler.
let proxy = new Proxy(target, handler);
If any incoming operation has a corresponding trap in the handler, then the proxy executes it. Otherwise, the original operation is executed on the target itself. Let’s look at some examples.
With empty handler — No traps
let target ={};
let handler = {}; // empty handlerlet proxy = new Proxy(target, handler);proxy.myName = "John"; // adds myName to proxy, set value to "John"console.log(proxy.myName); // reads "John" from proxy
console.log(target.myName); // reads "John" from from target
In the above example, there are no traps
in the handler
as it is empty. Due to that, all incoming operations are passed to the target
object.
We added a new property myName
and set the value to “John”. It is readable from both the proxy and the target. So it’s observable that without any traps proxy
is a transparent wrapper for target
.
With traps in the handler
Below is a simple example of a proxy with get
trap implementation.
let target = [1, 2, 3, 4]; // An array of numbers
let handler = {
get(obj, prop) {
return prop in obj? obj[prop] : 0;
}
};let proxy = new Proxy(target, handler);console.log(proxy[0]); // reads first element from proxy -> 1
console.log(target[0]); // reads first element from proxy -> 1
Let’s get back to proxy methods. In the above examples, we set new properties, we read properties as we do with get
or set
methods in objects. In the lower level, there are internal methods specified for object operations. For instance, [[Get]]
, reads a property and [[Set]]
, writes to a property. Similarly, there are a number of internal methods listed in Proxy Specification where we can intercept with traps
.
Let’s look at a set
trap implementation.
Note that the
set
trap should returntrue
on a successful set execution, otherwisefalse
. If not returned either it will throw aTypeError
. The same conditions apply on[[Delete]]
as well. Likewise, the aforementioned specification imposes some conditions on several internal methods while writing traps on them.
let target = []; // An array of numbers
let handler = {
set(obj, prop, val) {
if(typeof val === 'number') {
obj[prop] = val;
return true;
} else {
return false;
}
}
};let proxy = new Proxy(target, handler);
proxy.push(1);
proxy.push(2);
console.log(target.length); // returns 2proxy.push("hello"); // TypeError : 'set' on proxy: trap returned false
Reflect
Reflect
makes our lives easier by simplifying the creation of Proxy
. It’s like syntactic sugar for Proxy
object creation and use. With Reflect
we can simply pass on the parameters along with the corresponding method invocation.
In Proxy
, handler methods are triggered on certain events that cause it to happen. For instance, construct
handler is triggered with new
operator, get
is triggered while reading a property and set
is triggered while writing to a property. Likewise, Reflect
facilitates us to call such operators as functions. Below are a few examples of such function definitions.
Reflect.get(obj, prop) // reading
Reflect.set(obj, prop, value) // writing
Reflect.deleteProperty(obj, prop) // deleting a property
Reflect.construct(F, value) // invoke new to create an object
See the following example of set in use.
let student = {};Reflect.set(student, 'name', 'John');console.log(student.name); // John
Simply for every internal method, which can be trapped with Proxy
has a corresponding function in Reflect
having the same name and arguments defined in Proxy
trap.
Limitations
Given that proxies can provide us a way to tweak the behavior of existing objects, there are downsides or limitations over some of the internal behaviors of built-in objects/elements.
Built-in objects using Internal slots
Many built-in objects like Map
, Set
, Promise
or Date
utilize hidden/reserved properties called “internal slots” which cannot be intercepted using Proxy
. For instance, Map
stores items in [[MapData]]
internal slot which are directly accessible via built-in methods, but not via [[Get]]
. If we try to intercept that kind of object, it will fail since Proxy
doesn’t have these internal slots or its behaviors.
Look at the following example.
let m = new Map();m.set('hello', 1); // {"hello" => 1}let proxy = new Proxy(m, {}); // empty handlerproxy.set('test', 2); // will give an error
We created a Map
and added a new key-value pair. Then we tried the same on the proxied map and that gave us an error. Why? Because Map
stores its data in [[MapData]]
an internal slot. Our proxy
doesn’t have such a slot. When the built-in Map.prototype.set
tried to access this.[[MapData]
internal property where this = proxy
and the proxy
which doesn’t have one as such it returned an error.
We can overcome this situation by adding get
trap which binds the function properties to the target object Map
(m) itself. Observe the following example.
let m = new Map();let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});proxy.set('hello', 1); // Works fine
console.log(proxy.get('hello')); // Returns -> 1
Now you might have a doubt on “Okay, how this works for Array?”
The answer is simple because the built-in Array doesn’t use internal slots.
Private fields will be new in JavaScript Classes which are also implemented using internal slots. As a matter of fact, a proxied object instance will throw an error if we try to access a private property via a get method.
class Student {
#nickName = "Nicky"; // Already set for the getter demonstration
getNickName() {
return this.#nickName;
}
}let student = new Student();student = new Proxy(student, {});console.log(student.getNickName()); // Error
Note that [[Get]]
or [[Set]]
are not used while accessing private fields.
This situation can also be overcome by the previous strategy itself.
class Student {
#nickName = "Nicky"; // Already set for the getter demonstration
getNickName() {
return this.#nickName;
}
}let student = new Student();student = new Proxy(student, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});console.log(student.getNickName()); // Error
Important!!!
Proxy != target
Although we intercept the target using a Proxy, those objects are not the same. As explained earlier, it’s just a gateway to our target object.
Example:
let students = new Set();class Student {
constructor(name) {
this.name = name;
students.add(this);
}
}let student = new Student("Bob");
console.log(students.has(student)); // returns truestudent = new Proxy(student, {}); // re-assign with Proxy
console.log(students.has(student)); // returns false
Use Cases
Following is a use case where Proxy or Reflect can be useful. You can look for more use cases here.
Validation
You can add custom validation checks for any object with a Proxy
by trapping Set
handler.
let validator = {
set(obj, prop, value) {
if (prop === 'mark' && !Number.isInteger(value)) {
throw new TypeError('The mark is not an integer');
} // store the value
obj[prop] = value;// trap should return true on success
return true;
}
};let subject = new Proxy({}, validator);subject.mark = 100;
console.log(subject.mark); // 100
subject.mark = '100'; // Throws an exception
You can seek other trappable methods in addition to the methods mentioned in this article. Hope you learned something useful.