Like I did in previous articles on the series, I would love to point out that you should probably set up Babel and follow along the examples with either a REPL or the
babel-node
CLI and a file. That’ll make it so much easier for you to internalize the concepts discussed in the series. If you aren’t the “install things on my computer” kind of human, you might prefer to hop on CodePen and then click on the gear icon for JavaScript – they have a Babel preprocessor which makes trying out ES6 a breeze. Another alternative that’s also quite useful is to use Babel’s online REPL – it’ll show you compiled ES5 code to the right of your ES6 code for quick comparison.
Before getting into it, let me shamelessly ask for your support if you’re enjoying my ES6 in Depth series. Your contributions will go towards helping me keep up with the schedule, server bills, keeping me fed, and maintaining Pony Foo as a veritable source of JavaScript goodies.
Thanks for reading that, and let’s go into Reflect
. I suggest you read the articles on proxies: Proxy
built-in, traps, and more traps. These will help you wrap your head around some of the content we’ll go over today.
Why Reflection?
Many strongly typed languages have long offered a reflection API (such as Python or C#), whereas JavaScript hardly has a need for a reflection API – it already being a dynamic language. The introduction of ES6 features a few new extensibility points where the developer gets access to previously internal aspects of the language – yes, I’m talking about Proxy
.
You could argue that JavaScript already has reflection features in ES5, even though they weren’t ever called that by either the specification or the community. Methods like Array.isArray
, Object.getOwnPropertyDescriptor
, and even Object.keys
are classical examples of what you’d find categorized as reflection in other languages. The Reflect
built-in is, going forward, going to house future methods in the category. That makes a lot of sense, right? Why would you have super reflectiony static methods like getOwnPropertyDescriptor
(or even create
) in Object
? After all, Object
is meant to be a base prototype, and not so much a repository of reflection methods. Having a dedicated interface that exposes most reflection methods makes more sense.
Reflect
We’ve mentioned the Reflect
object in passing the past few days. Much like Math
, Reflect
is a static object you can’t new
up nor call, and all of its methods are static. The _traps in ES6 proxies (covered here and here) are mapped one-to-one to the Reflect
API. For every trap, there’s a matching reflection method in Reflect
.
The reflection API in JavaScript has a number of benefits that are worth examining.
Return Values in Reflect
vs Reflection Through Object
The Reflect
equivalents to reflection methods on Object
also provide more meaningful return values. For instance, the Reflect.defineProperty
method returns a boolean value indicating whether the property was successfully defined. Meanwhile, its Object.defineProperty
counterpart returns the object it got as its first argument – not very useful.
To illustrate, below is a code snippet showing how to verify Object.defineProperty
worked.
try {
Object.defineProperty(target, 'foo', { value: 'bar' })
// yay!
} catch (e) {
// oops.
}
As opposed to a much more natural Reflect.defineProperty
experience.
var yay = Reflect.defineProperty(target, 'foo', { value: 'bar' })
if (yay) {
// yay!
} else {
// oops.
}
This way we avoided a try
/catch
block and made our code a little more maintainable in the process.
Keyword Operators as First Class Citizens
Some of these reflection methods provide programmatic alternatives of doing things that were previously only possible through keywords. For example, Reflect.deleteProperty(target, key)
is equivalent to the delete target[key]
expression. Before ES6, if you wanted a method call to result in a delete
call, you’d have to create a dedicated utility method that wrapped delete
on your behalf.
var target = { foo: 'bar', baz: 'wat' }
delete target.foo
console.log(target)
// <- { baz: 'wat' }
Today, with ES6, you already have such a method in Reflect.deleteProperty
.
var target = { foo: 'bar', baz: 'wat' }
Reflect.deleteProperty(target, 'foo')
console.log(target)
// <- { baz: 'wat' }
Just like deleteProperty
, there’s a few other methods that make it easy to do other things too.
Easier to mix new
with Arbitrary Argument Lists
In ES5, this is a hard problem: How do you create a new Foo
passing an arbitrary number of arguments? You can’t do it directly, and it’s super verbose if you need to do it anyways. You have to create an intermediary object that gets passed the arguments as an Array
. Then you have that object’s constructor return the result of applying the constructor of the object you originally intended to .apply
. Straightforward, right? – What do you mean no?
var proto = Dominus.prototype
Applied.prototype = proto
function Applied (args) {
return Dominus.apply(this, args)
}
function apply (a) {
return new Applied(a)
}
Using apply
is actually easy, thankfully.
apply(['.foo', '.bar'])
apply.call(null, '.foo', '.bar')
But that was insane, right? Who does that? Well, in ES5, everyone who has a valid reason to do it! Luckily ES6 has less insane approaches to this problem. One of them is simply to use the spread operator.
new Dominus(...args)
Another alternative is to go the Reflect
route.
Reflect.construct(Dominus, args)
Both of these are tremendously simpler than what I had to do in the dominus
codebase.
Function Application, The Right Way
In ES5 if we want to call a method with an arbitrary number of arguments, we can use .apply
passing a this
context and our arguments.
fn.apply(ctx, [1, 2, 3])
If we fear fn
might shadow apply
with a property of their own, we can rely on a safer but way more verbose alternative.
Function.prototype.apply.call(fn, ctx, [1, 2, 3])
In ES6, you can use spread as an alternative to .apply
for an arbitrary number of arguments.
fn(...[1, 2, 3])
That doesn’t solve your problems when you need to define a this
context, though. You could go back to the Function.prototype
way but that’s way too verbose. Here’s how Reflect
can help.
Reflect.apply(fn, ctx, args)
Naturally, one of the most fitting use cases for Reflect
API methods is default behavior in Proxy
traps.
Default Behavior in Proxy
Traps
We’ve already talked about how traps are mapped one-to-one to Reflect
methods. We haven’t yet touched on the fact that their interfaces match as well. That is to say, both their arguments and their return values match. In code, this means you could do something like this to get the default get
trap behavior in your proxy handlers.
var handler = {
get () {
return Reflect.get(...arguments)
}
}
var target = { a: 'b' }
var proxy = new Proxy(target, handler)
console.log(proxy.a)
// <- 'b'
There is, in fact, nothing stopping you from making that handler
even simpler. Of course, at this point you’d be better off leaving the trap out entirely.
var handler = {
get: Reflect.get
}
The important take-away here is that you could set up a trap in your proxy handlers, wire up some custom functionality that ends up throwing or logging a console statement, and then in the default case you could just use the one-liner recipe found below.
return Reflect[trapName](...arguments)
Certainly puts me at ease when it comes to demystifying Proxy
.
Lastly, There’s __proto__
Yesterday we talked about how the legacy __proto__
is part of the ES6 specification but still strongly advised against and how you should use Object.setPrototypeOf
and Object.getPrototypeOf
instead. Turns out, there’s also Reflect
counterparts to those methods you could use. Think of these methods as getter and setters for __proto__
but without the cross-browser discrepancies.
I wouldn’t just hop onto the "
setPrototypeOf
all the things" bandwagon just yet. In fact, I hope there never is a train pulling that wagon to begin with.
Comments