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
.
1. Metaprogramming
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.
1.1. When the Meta Language and The Target Language Are Different
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.)
1.2. When The Meta Language and The Target Language Are Same
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.
Now, let’s discuss reflection, a branch of metaprogramming.
1.3. Reflective Programming
Both Proxy
and Reflect
are implementations of reflection, and the reflection has three types.
- Type Introspection
This is when the program accesses its structure in runtime to learn a type or an attribute.Object.keys()
is an example of such case. - Self-modification
As the name suggests, it means that the program can change its own structure. Some examples are using the square brackets ([]
) to get access to an attribute or using thedelete
operator to remove an attribute. - Intercession
Intercession is an act of getting in the way on behalf of other, and programming wise, it means redefining some of the ways a language is executed. According to Axel Rauschmayer, the owner of 2ality, the ES2015’sProxy
was built to support this feature. (While we can't say that such support never existed because of methods likeObject.defineProperty()
, but if we focus on the fact that the intercession does not alter the target, he may be right.)
2. What Is A 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.
2.1. Creating the Proxy Object
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 intercession
The code above is just an empty Proxy object. The variable p
has the following structure.
Slots 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 revoked
The 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.
2.2. Trap
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.
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.
2.3. Revocable Proxy Objects
Proxy object built using a constructor cannot be garbage collected nor reused. Therefore, a revocable Proxy can be built, if necessary.
The revocable()
method returns a new object that contains the Proxy object with the revoke()
method.
The 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.
3. What is Reflect
?
Reflect
is one of the built-in objects that provide methods that can intercept JavaScript commands like the Proxy
.
3.1. Characteristics of Reflect
Reflect
is a regular object, not a function object.- It has an internal slot of
[[Prototype]]
, and its value isObject.prototype
. (Reflect.__proto__ === Object.prototype
) - It does not have a
[[Construct]]
slot, so it cannot be called with anew
operator. - It does not have a
[[Call]]
slot, so it cannot be called as a function. - Every trap supported with
Proxy
is also supported forReflect
with a built-in method through the same interface.
3.2. Utility of Reflect
- API Collected in a Single Namespace The
Reflect
namespace allows for more intuitive reflect APIs to be used compared toObject
. - Cleaner Code You can implement error handling and reflection through
Reflect
with a cleaner code.
- More Stable Call If you have worked with a strict ESLint Rule Preset, you may have came across no-prototype-builtins rule. This rule exists to prevent bugs that can occur when the method name of a default built in object is the name of the attribute of an instance method. It prevents developers from using methods from
Object.prototype
in instances. This means that you can't useobj.hasOwnProperty
and that you have to write outObject.prototype.hasOwnProperty.call
, but you can useReflect
to write concisely and follow the rule at the same time.
3.3. Reflect.get
and Reflect.set
Now, let’s briefly go over Reflect.get
and Reflect.set
.
3.3.1. 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.
3.3.2. Reflect.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.
3.4. Interlude: receiver
In 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.
3.4.1. ECMAScript’s Property Lookup Process
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.)
ECMAScript specs examine the objects’ properties in the following ways.
- Read as
MemberExpression.IdentifierName
andMemberExpression.[Expression]
. - After reading, call
GetValue(V)
and then the[[Get]](P, Receiver)
inner slot method.
Now let’s look at the following code.
The reading process is as follows:
- When
GetValue(V)
is called, theP
is thejob
property we are looking for, and theReceiver
, the resulting value ofGetThisValue(V)
becomesthis
's context value,child
. [[Get]](P, Receiver)
is called, and then,OrdinaryGet(O, P, Receiver)
is called.O
is thechild
;P
isjob
; andReceiver
ischild
.- Since
child
does not have thejob
, it recursively callsparent.[[Get]](P, Receiver)
via prototype chaining. Here, theReceiver
is passed on as is. - Then, the
OrdinaryGet(O, P, Receiver)
is called, andO
becomes theparent
. Afterwards, it looks forjob
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.
3.4.2. When Is The 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.
The code above works as follows.
child.[[Get]](P, Receiver)
is called, and theReceiver
becomes thechild
.- According to prototype chaining, the
parent.[[Get]](P, Receiver)
is called recursively and when theage
, thegetter
is ran, theReceiver
is used asthis
.
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
.
3.5. Reflect.get
And Reflect.set
's receiver
The 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.
The following example uses the receiver
with Reflect.set
.
Finally, 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.
4. Why Reflect
Was Used With Proxy
Now, 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.
4.1. What If There Were No Reflect
The 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.
The following code is based on the code above. The example modifies a child
that has a reactivityParent
, a Proxy object, as its prototype.
the get
trap:
- Once the program searches for the
age
inchild
, the search continues through the Proxy object via prototype chaining. (Reference - step 3) - When the
parent
's[[Get]]
is called, theProxy
'sget
trap is triggered, and because thetarget
inside the trap is theparent
, when the program looks fortarget[key]
, it is identical to evaluatingparent.age
. Therefore, thethis
becomes theparent
.
the set
trap:
- Once you assign the
child
'sjob
property to be'unemployed'
, the search continues through the Proxy object via prototype chaining. (Reference - step 2) - When the
parent
's[[Set]]
is called, theProxy
'sset
trap is triggered, and because thetarget[key]
isparent['job']
, thejob
property is added and is assigned to theparent
.
Here, the child
's age is set to 40
, and the job is assigned in the reactivityParent
. Something's not right.
4.2. Using the receiver
Through Reflect
Now, 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.
the get
trap:
- As in code 4.1., the
Proxy
'sget
trap is triggered. - In order to get the value, the program calls the
Reflect.get
, and the actual code becomesReflect.get(parent, 'age', child)
. - When the
parent
'sget age()
is called,this
is bound to thechild
, and the instructions are carried out with respect to thechild
'sage
.
the set
trap:
- As in code 4.1., the
Proxy
'sset
trap is triggered. - In order to set the value, the program calls the
Reflect.set
, and the actual code becomesReflect.set(parent, 'age', 'unemployed',child)
. - The program passed the
receiver
and calledReflect.set
, so the actual target of the process becomes thechild
.
Now, 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!
References
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