The global `Reflect` object, its use cases and things to watch out for

4 min read

This post is part of my Today I learned series in which I share all my learnings regarding web development.

I was reading the source code of one of Sindre Sorhus' modules today. It was the module on-change which provides the functionality of watching changes performed on Objects or Arrays. The module doesn't include much code – as it's very often the case for Sindre's modules. They serve a single purpose and usually are quick to use high-quality utilities.

The whole module is 23 lines of code.

'use strict';

module.exports = (object, onChange) => {
    const handler = {
        get(target, property, receiver) {
            try {
                return new Proxy(target[property], handler);
            } catch (err) {
                return Reflect.get(target, property, receiver);
            }
        },
        defineProperty(target, property, descriptor) {
            onChange();
            return Reflect.defineProperty(target, property, descriptor);
        },
        deleteProperty(target, property) {
            onChange();
            return Reflect.deleteProperty(target, property);
        }
    };

    return new Proxy(object, handler);
};

I expected the usage of Proxy in this module, but there are things in this code that have been new to me. These are the reason for me writing this post today. πŸŽ‰

First of all, the code uses a global Reflect object which I haven't seen before. I headed to MDN to look at the definition.

Reflect is a built-in object that provides methods for interceptable JavaScript operations. These methods are the same as those of proxy handlers. Reflect is not a function object, so it's not constructible.

For me the definition was not really helpful at this point because I was looking for an answer to the question "Why should I use it?".

Side note: should the information why we have a global Reflect object be on MDN? Neither the MDN entry nor the EcmaScript spec paragraph answer that question.

After digging and googling a while, I came again across the fact that Reflect includes the same methods as the defined traps being available in a proxy in a StackOverflow thread.

These methods are:

  • apply()
  • construct()
  • defineProperty()
  • deleteProperty()
  • get()
  • getOwnPropertyDescriptor()
  • getPrototypeOf()
  • has()
  • isExtensible()
  • ownKeys()
  • preventExtensions()
  • set()
  • setPrototypeOf()

The primary use case of the Reflect object is it to make it easy to interfere functionality of an existing object with a proxy and still provide the default behavior. You can always just use the defined counterpart to the Proxy handler in the Reflect object and can be sure that the default behavior stays the same.

const loggedObj = new Proxy(obj, {
  construct: function(target, argumentsList) {
    // additional functionality
    // ...
    return Reflect.construct(target, argumentsList);
    // ☝️ same as `return new target(...argumentsList);`    
  },
  get: function(target, name) {
    // additional functionality
    // ...
    return Reflect.get(target, name);
    // ☝️ same as `return target[name];`
  },
  deleteProperty: function(target, name) {
    // additional functionality
    // ...
    return Reflect.deleteProperty(target, name);
    // ☝️ same as `return delete target[name];`
  }
});

These methods are very convenient because you don't have to think of syntactic differences in JavaScrict for specific operations and can just use the same method defined in Reflect when dealing with proxies.

But there is more...

You might have noticed that some methods defined in the Proxy object have the same name as functions defined in the Object prototype. These look the same but can behave slightly differently. So you have to watch out there.

defineProperty is a good example. It behaves differently in case a property can't be defined on an object.

// setup
const obj = {};
Object.defineProperty(obj, 'foo', {configurable: false, value: 42});

// differences
Object.defineProperty(obj, 'foo', {value: 43});
// ☝️ this throws `can't redefine non-configurable property "foo"`

Reflect.defineProperty(obj, 'foo', {value: 43});
// ☝️ this returns `false`

With this knowledge let's have another look at Sindre's module again, now with added comments.

'use strict';

module.exports = (object, onChange) => {
  const handler = {
    get(target, property, receiver) {
      try {
        // this goes recursively through the object and 
        // creates new Proxies for every object defined
        // in the target object when it is accessed
        // 
        // e.g. `a.b.c = true` triggers: 
        // - `get` for accessing `b`
        // - `defineProperty` for setting `c`
        return new Proxy(target[property], handler);
      } catch (err) {
        // ☝️ throws when a new Proxy is iniatlized with a string or a number
        // which means that `Reflect.get` does the job
        return Reflect.get(target, property, receiver);
      }
    },
    defineProperty(target, property, descriptor) {
      // notify about changes
      onChange();
      // use `Reflect.defineProperty` to provide default functionality
      return Reflect.defineProperty(target, property, descriptor);
    },
    deleteProperty(target, property) {
      // notify about changes
      onChange();
      // use `Reflect.deleteProperty` to provide default functionality
      return Reflect.deleteProperty(target, property);
    }
  };

  return new Proxy(object, handler);
};

And that's it for today. I can only recommend reading small modules like this one from time to time. I find useful and new stuff very often.

Special thanks to the StackOverflow user GitaarLAB. The answer in the mentioned thread was extremely useful and I admire people that take the time to "craft" detailed answers to help people out!

Load time