Reflection in JavaScript

Reflection is defined as the ability of a program to inspect and modify its structure and behavior at runtime. In this article we will explore some features of JavaScript related to reflection, and suggest usages for them.

Inferring types

JavaScript has offered reflection for a long time now, with operators such as typeof and instanceof :

typeof is used to know the type of an unevaluated value, which can be a primitive or an object (note that functions and arrays are also objects).

is used to know the type of an unevaluated value, which can be a primitive or an object (note that functions and arrays are also objects). instanceof is used to know if the prototype of a constructor is in an object's prototype chain. This requires the instanceof operator to evaluate properties from both objects.

Let’s take a look at instanceof :

// Definition of Fruit

function Fruit() {

this.isFood = true;

}; // Definition of Banana using old-style inheritance

function Banana() {

this.name = 'Banana';

} Banana.prototype = Object.create(Fruit.prototype); // High level inspections

const banana = new Banana();

console.log(banana instanceof Banana); // true, Banana is a Fruit

console.log(banana instanceof Fruit); // true, Fruit is an Object

console.log(banana instanceof Object); // true

a instanceof B can be oversimplified as B[Symbol.hasInstance](a) :

console.log(Banana[Symbol.hasInstance](banana)); // true

console.log(Fruit[Symbol.hasInstance](banana)); // true

console.log(Object[Symbol.hasInstance](banana)); // true

NOTE: Symbol is used to create globally unique values. The reason why symbols are used as identifiers rather than strings is to reduce identifier collisions with user code.

B[Symbol.hasInstance](a) can be understood as traversing the prototype chain of a until B.prototype is found.

// mostly equivalent to Banana[Symbol.hasInstance](banana)

console.log(

Reflect.get(Banana, ‘prototype’) === (

Reflect.getPrototypeOf(banana)

)

); // true // mostly equivalent to Fruit[Symbol.hasInstance](banana)

console.log(

Reflect.get(Fruit, 'prototype') === (

Reflect.getPrototypeOf(

Reflect.getPrototypeOf(banana)

)

)

); // true // mostly equivalent to Object[Symbol.hasInstance](banana)

console.log(

Reflect.get(Object, 'prototype') === (

Reflect.getPrototypeOf(

Reflect.getPrototypeOf(

Reflect.getPrototypeOf(banana)

)

)

)

); // true

NOTE: When I say “mostly equivalent”, is because this an oversimplified version of what the built-in function does according to the ECMAScript spec.

Altering the behavior of instanceof

Now, we know that:

a instanceof B is expressed in terms of B[Symbol.hasInstance] .

is expressed in terms of . B[Symbol.hasInstance] works by iterating the prototype chain of a .

We can override the behavior of [Symbol.hasInstance] :

class C {

// `x instanceof C` will now be true

static [Symbol.hasInstance](instance) {

return true;

}

}; console.log({} instanceof C); // true

console.log(Date instanceof C); // true

console.log(Promise instanceof C); // true

We can also use Proxy to return an alternative prototype, when evaluated through the proxy:

const obj = {};

const proxy = new Proxy(obj, {

getPrototypeOf: function () {

return Date.prototype;

}

}) // prints true

console.log(proxy instanceof Date); // also prints true

console.log(Reflect.getPrototypeOf(proxy) === Date.prototype);

More usages of proxies

Proxy can be used also to override more fundamental behaviors. Proxies are created like this:

new Proxy(target, [traps])

The second argument is an object containing handler functions for various operations, these include:

get / set : getting/setting a property.

/ : getting/setting a property. defineProperty / deleteProperty : defining/deleting a property.

/ : defining/deleting a property. has : overrides behavior of in operator.

: overrides behavior of operator. getPrototypeOf / setPrototypeOf : Obtaining or setting an object's prototype.

/ : Obtaining or setting an object's prototype. apply / construct : invoking a function, or overriding the new operator.

/ : invoking a function, or overriding the operator. ownKeys : obtaining the keys of an object. e.g: Object.keys , Reflect.ownKeys , etc.

: obtaining the keys of an object. e.g: , , etc. and more…

Not all operations can be proxied. For instance, support for proxying for..in enumerations was deprecated. This is also the case for iterating over an array with a for loop.

Beyond proxies

In addition to using proxies, methods named using built-in symbols can be used to override additional fundamental behavior. Among them, we have:

[Symbol.hasInstance] : explained earlier in the instanceof section.

: explained earlier in the section. [Symbol.iterator] : used by for..of loops and spread syntax.

: used by loops and spread syntax. [Symbol.toPrimitive] : used to convert the object into a primitive given a type hint argument. Similar to valueOf() (which does not take a hint) and the better known toString() .

Testing applications

Overriding fundamental JavaScript behavior for an object can be useful when testing an application:

Inspecting a function call in terms of its arguments list or receiver.

Having detailed information of how and when a property is accessed, defined or deleted.

Satisfying type constraints without having to go through the process of defining a type or instancing a type.

There are existing libraries that help with some of these tasks, such as sinon . However, this creates the need for additional dependencies, and while sinon is rather robust, the features mentioned in this article are well integrated into modern JavaScript.

For example, we can use the following proxy to validate a function call:

// Using Node's `assert` module

const assert = require('assert'); // Application code

const targetObj = {

// targetFn signature is (number, boolean, object) => number

targetFn: (a, b, c) => { return 123; }

}; // Test code

const proxy = new Proxy(targetObj, {

// This handler will run when a property is read from `targetObj`

get: (targetObj, prop, receiver) => {

const value = Reflect.get(targetObj, prop); if (prop !== 'targetFn') {

return value;

} return new Proxy(value, {

// This handler will run when targetObj.targetFn() is invoked

// - fn is the function being invoked

// - thisArg is the receiver object

// - args is the arguments list

apply: (fn, thisArg, args) => {

// Validate that the receiver is an object

assert.ok(thisArg instanceof Object); // Validate function argument types

assert.strictEqual(args.length, 3);

assert.strictEqual(typeof args[0], 'number');

assert.strictEqual(typeof args[1], 'boolean');

assert.strictEqual(typeof args[2], 'object');

assert.ok(typeof args[2]);

assert.ok(args[2] instanceof Object); // Validate that the results

const result = Reflect.apply(fn, thisArg, args);

assert.strictEqual(result, 123);

return result;

}

});

}

}); // Usage

proxy.targetFn(1, true, {});

Conclusion

JavaScript provides powerful features for examining and modifying objects and their interactions at runtime. In addition to testing, this functionality can be useful when logging, debugging, as well as writing adapters and shims to retain compatibility among versions. I invite you to explore more creative usages for these features.