Metaprogramming is a powerful tool in any language, and with the advent of ES6, there are even more options for metaprogramming in JavaScript now! ES6 added Reflect
and Proxy
to its repertoire of built-in objects, which allow for dynamic behavior in your code.
At its core, metaprogramming is simple—it just means programs that can write programs. Think of it as code that can change itself or other code while running.
As you could imagine, metaprogramming is very powerful, but also very dangerous.
- Changing the behavior of code you don’t control—for example, adding logging, validation, or other utility code to a third party library.
- Making a program more flexible, for example, adding in backwards compatibility, or instituting a switch based on a variable set at runtime.
- Metaprogramming is a complicated solution. Simple solutions are better 95% of the time.
- Metaprogramming obfuscates the code flow—sometimes it’s hard to know what is being run just by looking at the code. It might take debugging and stepping through the code just to figure out what it does.
- You can’t rely on using ES6
- You need to run the code in Internet Explorer.
Reflection is a crucial part of metaprogramming. It allows for access to an object and its methods and attributes at runtime.
For example, we might have some object const obj = { x: 1 };
Using conventional programming, we might run the following code:
console.log(obj.x);
obj.x = 2;
obj.someMethod();
Using Reflect
we might run it this way:
console.log(Reflect.get(obj, 'x'));
Reflect.set(obj, 'x', 2);
Reflect has corresponding methods for any object access methods, as well as methods for calling functions as well.
If you’re thinking that that just looks more verbose and awkward, you’re right. Remember what we said about meta programming being a complicated solution? That said, there are some rare instances where it’s indispensable.
Cases where Reflect might be the best choice:
- Use alongside the Proxy Object.
- Cases where you might be setting or applying a dynamic attribute—where you don’t necessarily know what you’ll be asking for until runtime.
The Proxy object is a way to intercept or ‘trap’ fundamental behavior on JavaScript objects, providing an entry point into that action to inspect or even redefine that behavior.
The proxy object takes two arguments. 1: the target
, which is the object we want to inspect. 2: the handler
which is a collection of traps. The supported traps include set
, which intercepts behavior to change a property on the object, get
, which intercepts access of a property, apply
which will intercept function calls, and others.
If you’re thinking to yourself that Proxy methods share their names with a lot of Reflect methods, you’re exactly right. While there may be some subtle differences, Proxy and Reflect mirror each other closely.
Examples where Proxy is useful:
- You need to augment existing behavior on an object—adding validation, modifying access to fields, etc.
- You need to redirect code based on some variable that is determined at runtime.
Have you ever logged out the contents of an object in 47 different places in your code, trying to see where object.foo
got changed from ’bar’
to ’baz’
? Worse still, does that object get mutated in some third party code stashed away in node_modules
? I know I have, at least twice in the last six months alone.
Here’s an example of a method using both Proxy
and Reflect
that will log changes to the fields of an object:
const attachProxy = (target) => {
const handler = {
set(obj, prop, value) {
console.log('setting value of prop:', prop, 'to value:', value, 'obj', obj);
console.trace();
return Reflect.set(...arguments);
}
};
return new Proxy(target, handler);
}
Note that it uses the set
trap on Proxy to listen for any changes to properties, and then uses Reflect to actually apply those changes.
You can see how this might be useful in other cases—you could instead do validation in that handler, and only return the Reflection when validation passes. Since you can attachProxy to any object, you can even call it on third party code, like lodash
. That said, you must be able to replace the object reference with a reference to the proxy, which is a very clear limitation.
Here’s the method in action:
// usage:
const obj = { foo: 'bar' };
const proxiedObj = attachProxy(obj);
proxiedObj.foo = 'baz';
/* output:
VM192:4 setting value of prop: foo to value: baz obj {foo: "bar"}
VM192:5 console.trace
set @ VM192:5
(anonymous) @ VM217:4
*/
Best of luck in your metaprogramming adventures with Reflect and Proxy in your toolkit!