Proxy as the way of metaprogramming in JS
Important note: Described Proxy object is something different than one of the OOP patterns.
I’ve seen several odd JS characteristics ever since I started using it on a daily basis. One of these irritates me — undefined
rather than a code execution error when attempting to access properties that don’t exist. In this regard, JS behaves somewhat differently from my experience with Ruby:
and a Ruby equivalent:
The error throw is produced by Ruby, but JS returns undefined
. Imagine working on a large JS project and a simple property typo results in an app being broken because undefined
was used instead of the key not found throw. You would not know why or where this happened because undefined
may appear throughout many different places (debugging undefined may take a long time). Fortunately, the built-in Proxies objects provided by the ES6+ standard provide a solution to the concern.
Since the release of ES6, JS is known as fully reflective programming language as the Reflection API has been advanced.
Reflection is the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime.
That implies that a program can execute on each of the three levels mentioned. Please be aware that ES5 has provided the potential of reflective introspection and self-modification - Object.keys()
for introspection or Object#delete
for self-modification - also all Object.*
methods are taken as reflective for metaprogramming, but neither they nor other ES5 features support the third level of reflection - behavioral level which is the reason for the introduction of proxies in ES6 that alter built-in language operations.
The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). (official docs)
Using proxies is a way for virtualizing objects eg. POJOs. Virtualized object peeks the same as a given object, and any operation on a given one directs to an already created virtualized by a proxy object. By virtualization, we can take control of standard methods default behavior by intercepting invocations and re-defining them.
Nothing special above, virtualization of the object and the property lookup without invoking any operations (see: no-op forwarding). When you add a property to an object, the same property is added to the proxy object. Although it is a symlink, proxies are designed to intercept low-level operations on the target object.
Error instead of undefined
…when accessing non-existent property with the get
trap.
Let’s solve the issue that is raised at the beginning of the post. How can the default behavior, which terminates in undefined
be changed to an error caused by code execution like in Ruby or Python? Let’s start by describing the default behavior using a proxy object:
Let’s change the default behaviour:
I passed the trap the following three arguments: 1) the target object for the proxy is denoted the trapTarget
2) the key
- the property and 3) the receiver
- the proxy reference. The js object Reflect
, which describes the default behavior of js. Please note there is a Reflect
technique for each proxy trap.
To recap, operations can be intercepted using a handler and a proxy trap, which is a function that is always supplied as the second proxy argument and is in charge of the operation.
I’ve shown two branches on the diagram: the default behavior and the proxy’s interception of the default behavior. On the most fundamental level, default reflection yields undefined
. The same outcome is also possible with a proxy.
The second branch - Proxy
intercepts default behaviour by get trap in the handler and raises an error in the code example if property doesn’t exist. Note: get
trap is one of many others traps - see all available traps for the Proxy
.
Building two-way data binding using Proxy
Moving on let’s dive into a more complex thing. If you’re familiar with AngularJS or VueJS you’re probably into two-way data binding concept as it’s the main philosophy of these frameworks (see AngularJS docs or VueJs example of v-model)
Two-way-data binding links the state with the view. If the state changes the view is updated and if the view changes the state will be updated.
Using Proxy is the way to go if you want to create your own two-way data binding-based js framework. Our view can be connected to application state through a proxy. Consider the following illustration:
The state and the view are now bound. The view alters as the state does. First, I gave the DOM input elements a brand-new attribute called data-model
. The key component of two-way data binding is the model, which connects input value and app state. After that, I created the straightforward state interface with two keys (name and hobby). It’s good to note that only keys that have been set in the interface can be modified in a proxy; otherwise, an error will be raised if eg. state.strangerKey = "Hello"
. The next step is to build a proxy that has a set
trap in the handler; updateView
is added between calls to the default engine set behavior, which means that each time the state is attempted to be changed, the input values in the view will also be altered. From a view to a state direction. Also listeners have been provided to the view that detect input value changes and trigger the state change. So registering listeners on DOM elements is crucial because only registered listeners can change the state via the onInputChange
event handler.
For now, the journey with Proxy wound up, I’ve presented the most common proxy traps - get
and set
but keep in mind that Proxy supports twelve more handlers which can be used for different purposes, especially in metaprogramming.
PS. Metaprogramming rocks :).