I began to wonder why Reflect was used to implement the Vue 3 Reactivity's Proxy trap. Upon Googling, only thing I could find was how to use the Proxy's handlers or other tutorials, so I decided to take a deeper look into it. This article reviews the basic concepts of Proxy and Reflect as well as explore why Reflect was used with the Proxy.
Before we dive headfirst into Proxy, allow me to introduce another topic. Proxy is a syntax that is built to support some feature within programming languages-the Metaprogramming.
Metaprogramming is when one computer program treats another program as data. In other words, a program is designed to read another program to either analyze or transform it. The language used for the metaprogramming is called meta language, and the target language is the language that is the target of manipulation. Meta language and target language may or may not be same.
The following example builds HTML from JSP. The meta language is Java, and the target language is HTML. (It's meant to be a simple example, but because the target language is HTML, it's a little bit weird. Just pretend you're using <script> inside it.)
out.println("<html><body>");
out.println("<h1>Hello World</h1>");
out.println("The current time is : " + new java.util.Date());
out.println("</body></html>");For the following example, I used an eval() to use JavaScript for both the meta language and the target language. eval analyzes the argument in runtime. Cases like this, where one programming language becomes the meta language of itself, is called reflection, and it manages and edits the structure and actions of itself in runtime.
> eval('1 + 1')
2Now, let's discuss reflection, a branch of metaprogramming.
Both Proxy and Reflect are implementations of reflection, and the reflection has three types.
Object.keys() is an example of such case.[]) to get access to an attribute or using the delete operator to remove an attribute.Proxy was built to support this feature. (While we can't say that such support never existed because of methods like Object.defineProperty(), but if we focus on the fact that the intercession does not alter the target, he may be right.)Proxy?The Proxy object is used in place of the target object. Instead of using the target object directly, the Proxy object is used to transfer each process to the target object and then to return the results in code.

Such methods allow developers to use Proxy objects to redefine how JavaScript's basic commands work. It means that developers have control over how objects deal with certain commands. Most symbolic commands that can be controlled are attribute search, access, assignment, enumeration, and function call.
new Proxy(target, handler);The Proxy object must be created using the new keyword. Proxy object takes two following parameters.
target : The target object of the intercessionhandler : The handler (trap) object to be used for the intercessionconst p = new Proxy({}, {});The code above is just an empty Proxy object. The variable p has the following structure.
> p;
Proxy {}
> [[Handler]]: Object
> [[Target]]: Object
> [[IsRevoked]]: falseSlots like [[]] are what are known as JavaScript's internal slots. Such slots cannot be accessed via code.
[[Handler]] : Maps the second argument[[Target]] : The target to be proxied; first argument[[IsRevoked]] : Whether the object is revokedThe above is the most basic Proxy construction, and you must be aware that once you set the Proxy's target object, it cannot be changed.
The Proxy object mechanism acts through the trap function in order to redefine the target object's basic instructions. All traps are optional, and if there are no traps, then the proxy object has no particular action.
const target = { name: 'target' };
const p = new Proxy(target, {
get: () => console.log('access')
})Once a Proxy is created, the trap cannot be added or deleted, and if you need to change something about it, you must create a new Proxy. For more information regarding traps, refer to the MDN document.
Proxy object built using a constructor cannot be garbage collected nor reused. Therefore, a revocable Proxy can be built, if necessary.
const revocable = Proxy.revocable({}, {});The revocable() method returns a new object that contains the Proxy object with the revoke() method.
> revocable;
{proxy: Proxy, revoke: f}
> revocable.proxy;
Proxy {}
> [[Handler]]: Object
> [[Target]]: Object
> [[IsRevoked]]: falseThe revocable.proxy object is identical to the Proxy object built using a constructor. Once the revoke() method is called, the Proxy object's [[IsRevoked]] value is set to true. If the revoked Proxy objects are triggered again, the TypeError is thrown and the objects are garbage collected.
Reflect?Reflect is one of the built-in objects that provide methods that can intercept JavaScript commands like the Proxy.
ReflectReflect is a regular object, not a function object.[[Prototype]], and its value is Object.prototype. (Reflect.__proto__ === Object.prototype)[[Construct]] slot, so it cannot be called with a new operator.[[Call]] slot, so it cannot be called as a function.Proxy is also supported for Reflect with a built-in method through the same interface.ReflectReflect namespace allows for more intuitive reflect APIs to be used compared to Object.Reflect with a cleaner code.// 1. Error Handling
const obj = {};
try {
Object.defineProperty(obj, 'prop', { value: 1 });
console.log('success');
} catch(e) {
console.log(e);
}
const obj2 = {};
if (Reflect.defineProperty(obj2, 'prop', { value: 1 })) {
console.log('success');
} else {
console.log('problem creating prop');
}// 2. More readable code
const obj = { prop: 1 };
console.log(Reflect.has(obj, 'prop') === ('prop' in obj)); // trueObject.prototype in instances. This means that you can't use obj.hasOwnProperty and that you have to write out Object.prototype.hasOwnProperty.call, but you can use Reflect to write concisely and follow the rule at the same time.const obj = { prop: 1, hasOwnProperty: 2 };> obj.hasOwnProperty('prop');
Uncaught TypeError: obj.hasOwnProperty is not a function
> Object.prototype.hasOwnProperty.call(obj, 'prop');
true
> Reflect.has(obj, 'prop');
trueReflect.get and Reflect.setNow, let's briefly go over Reflect.get and Reflect.set.
Reflect.get(target, propertyKey [, receiver])Reflect.get returns target[propertyKey] by default. If the target is not an object, then it will throw a TypeError. This TypeError improves the ambiguity of JavaScript. For example, in the case of 'a'['prop'] will be evaluated as undefined, but Reflect.get will throw an actual error.
const obj = { prop: 1 };> Reflect.get(obj, 'prop');
1
> 'a'['prop'];
undefined
> Reflect.get('a', 'prop');
Uncaught TypeError: Reflect.get called on non-objectReflect.set(target, propertyKey, V [, receiver])Reflect.set works similarly to Reflect.get. However, as the name set suggests, it takes a V parameter to be assigned.
const obj = { prop: 1 };> Reflect.set(obj, 'prop', 2);
true
> obj.prop === 2;
true
> 'a'['prop'] = 1; // No Error
1
> Reflect.set('a', 'prop', 1);
Uncaught TypeError: Reflect.set called on non-objectreceiverIn order to facilitate the understanding of the next topics, Reflect get/set and receiver, let's talk about the Receiver in the context of ECMAScript's object property lookup.
First, let's go through the process of ECMAScript's property lookup. (As this part simply serves to facilitate your understanding, we will only discuss the flow of an Ordinary Object.)
const obj = { prop: 1 };
const propertyKey = 'prop';
console.log(obj.prop); // 1
console.log(obj[propertyKey]); // 1ECMAScript specs examine the objects' properties in the following ways.
MemberExpression.IdentifierName and MemberExpression.[Expression].GetValue(V) and then the [[Get]](P, Receiver) inner slot method.Now let's look at the following code.
const child = { age: 0 };
const parent = { age: 40, job: 'programmer' };
child.__proto__ = parent;
console.log(child.job); // programmerThe reading process is as follows:
GetValue(V) is called, the P is the job property we are looking for, and the Receiver, the resulting value of GetThisValue(V) becomes this's context value, child.[[Get]](P, Receiver) is called, and then, OrdinaryGet(O, P, Receiver) is called. O is the child; P is job; and Receiver is child.child does not have the job, it recursively calls parent.[[Get]](P, Receiver) via prototype chaining. Here, the Receiver is passed on as is.OrdinaryGet(O, P, Receiver) is called, and O becomes the parent. Afterwards, it looks for job here, and then returns 'programmer'.For more details regarding the spec, refer to this link, but the important thing is that the Receiver remains intact even after object lookup via prototype chaining.
Receiver Used?The Receiver is used only when the property found using OrdinaryGet(O, P, Receiver) is a getter, and is passed as the getter function's this value. Now, let's examine the following code.
const child = {
birthYear: 2019
};
const parent = {
birthYear: 1981,
get age() {
return new Date().getFullYear() - this.birthYear;
},
set birthYear(year) {
this.birthYear = year;
}
};
child.__proto__ = parent;
console.log(child.age); // 2 (With respect to 2021)
child.birthYear = 2017;
console.log(child.age); // 4 (With respect to 2021)The code above works as follows.
child.[[Get]](P, Receiver) is called, and the Receiver becomes the child.parent.[[Get]](P, Receiver) is called recursively and when the age, the getter is ran, the Receiver is used as this.Eventually, the Receiver reveals information regarding the object that received the initial process request in the prototype chaining. While the example only shows it working with [[Get]], the [[Set]] follows a similar process(Reference) to set the child as the setter's this.
Reflect.get And Reflect.set's receiverThe Receiver, as explained earlier, is the object that receives the process request directly. The receiver of Reflect.get and Reflect.set works as the this context when the target[propertyKey] is getter or setter. In other words, it is through this receiver that you can manage the this binding.
The following example applies the receiver in a different way in order to modify the this binding and to return a sum, differently.
const obj = {
a: 1,
b: 2,
get sum() {
return this.a + this.b;
}
};
const receiverObj = { a: 2, b: 3 };> Reflect.get(obj, 'sum', obj);
3
> Reflect.get(obj, 'sum', receiverObj);
5The following example uses the receiver with Reflect.set.
const obj = {
prop: 1,
set setProp(value) {
return this.prop = value;
}
};
const receiverObj = {
prop: 0
};> Reflect.set(obj, 'setProp', 2, obj);
true
> obj.prop;
2
> Reflect.set(obj, 'setProp', 1, receiverObj);
true
> obj.prop;
2
> receiverObj.prop;
1Finally, JavaScript maintains the record of the object with the initial property lookup request even through prototype chaining at the Receiver in cases of getter/setter, and you can use the receiver parameter with the Reflect get/set traps to control them.
Reflect Was Used With ProxyNow, let's finally discuss why Proxy and Reflect are used together. Evan You, the creator of Vue.js, mentions the Reflect in the Proxy's trap during an online lecture, saying that "while it's outside of the lecture's scope, the [Reflect] was used to deal with the prototype's side effects." Let's focus on what he means and see what happens when you use the reactive object, which is a Proxy object, as a prototype.
ReflectThe following code is taken from my previous Ins and Outs of Vue 3 Reactivity. I have taken the Proxy part out and changed it so that it didn't use Reflect. If we did not use Reflect and used a regular Proxy trap instead, then the program will throw errors because it does not know the target of the current search.
function reactive(target) {
const proxy = new Proxy(
target,
{
get(target, key, receiver) {
const res = target[key]; // Change
// track(target, key);
return res;
},
set(target, key, value, receiver) {
const oldValue = target[key];
target[key] = value; // Change
if (oldValue !== value) {
// trigger(target, key, value, oldValue);
}
return value;
}
}
)
return proxy;
}The following code is based on the code above. The example modifies a child that has a reactivityParent, a Proxy object, as its prototype.
const child = {
birthYear: 2019
};
const parent = {
birthYear: 1981,
get age() {
return new Date().getFullYear() - this.birthYear;
}
};
const reactivityParent = reactive(parent);
child.__proto__ = reactivityParent;the get trap:
age in child, the search continues through the Proxy object via prototype chaining. (Reference - step 3)parent's [[Get]] is called, the Proxy's get trap is triggered, and because the target inside the trap is the parent, when the program looks for target[key], it is identical to evaluating parent.age. Therefore, the this becomes the parent.the set trap:
child's job property to be 'unemployed', the search continues through the Proxy object via prototype chaining. (Reference - step 2)parent's [[Set]] is called, the Proxy's set trap is triggered, and because the target[key] is parent['job'], the job property is added and is assigned to the parent.> child.age; // (With respect to 2021)
40
> child.job = 'unemployed';
> child.hasOwnProperty('job');
false
> child.job;
'unemployed'
> reactivityParent.hasOwnProperty('job');
true
> reactivityParent.job;
'unemployed'Here, the child's age is set to 40, and the job is assigned in the reactivityParent. Something's not right.
receiver Through ReflectNow, let's use Reflect in the Proxy's get/set traps and use the receiver to pass on the actual object that received the process request as this context to get rid of the side effects.
function reactive(target) {
const proxy = new Proxy(
target,
{
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// track(target, key);
return res;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver);
if (oldValue !== res) {
// trigger(target, key, value, oldValue);
}
return res;
}
}
)
return proxy;
}
const child = {
birthYear: 2019
};
const parent = {
birthYear: 1981,
get age() {
return new Date().getFullYear() - this.birthYear;
}
};
const reactivityParent = reactive(parent);
child.__proto__ = reactivityParent;the get trap:
Proxy's get trap is triggered.Reflect.get, and the actual code becomes Reflect.get(parent, 'age', child).parent's get age() is called, this is bound to the child, and the instructions are carried out with respect to the child's age.the set trap:
Proxy's set trap is triggered.Reflect.set, and the actual code becomes Reflect.set(parent, 'age', 'unemployed',child).receiver and called Reflect.set, so the actual target of the process becomes the child.> child.age; // (With respect to 2021)
2
> child.job = 'unemployed';
> child.hasOwnProperty('job');
true
> reactivityParent.hasOwnProperty('job');
false
> child.job;
'unemployed'
> reactivityParent.job;
undefinedNow, the child's age is displayed accurately, and the job is assigned to be 'unemployed'!
What started out to discuss how Reflect is related Proxy, became more about understanding the receiver. I realize once more that, in order to understand the complexities of JavaScript, we must understand the ECMAScript specs. (No matter how much it makes me nauseous...)
Thank you for reading such a long article. The end!
https://en.wikipedia.org/wiki/Metaprogramming
https://en.wikipedia.org/wiki/Reflective_programming
https://exploringjs.com/es6/ch_proxies.html
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Reflect
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object
https://tc39.es/ecma262/#sec-reflection
https://tc39.es/ecma262/#sec-ordinaryget
https://tc39.es/ecma262/#sec-ordinaryset
https://github.com/tvcutsem/harmony-reflect/wiki
https://v8.dev/blog/understanding-ecmascript-part-2