ponyfoo.com

ES6 Reflection in Depth

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

Oh hey – I was just casually getting ready, didn’t see you there! Welcome to another edition of ES6 – “Oh. Good. We survived traps” – in Depth. Never heard of it? Refer to A Brief History of ES6 Tooling. Then, make your way through destructuring, template literals, arrow functions, the spread operator and rest parameters, improvements coming to object literals, the new classes sugar on top of prototypes, let, const, and the “Temporal Dead Zone”, iterators, generators, Symbols, Maps, WeakMaps, Sets, and WeakSets, proxies, proxy traps, and more proxy traps. We’ll be touching on the Reflect API today.

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.

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments