Reflection in JavaScript

Proxies and Reflection in JavaScript

Intrinsic
intrinsic

--

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).
  • 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].
  • 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.
  • defineProperty/deleteProperty: defining/deleting a property.
  • has: overrides behavior of in operator.
  • getPrototypeOf/setPrototypeOf: Obtaining or setting an object's prototype.
  • apply/construct: invoking a function, or overriding the new operator.
  • ownKeys: obtaining the keys of an object. e.g: Object.keys, Reflect.ownKeys, 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.
  • [Symbol.iterator]: used by for..of 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.

This article was written by me, Felipe Ortega. I work at a company called Intrinsic where we specialize in writing software for securing Node.js applications using the Least Privilege model. Our product is very powerful and is easy to implement. If you are looking for a way to secure your Node applications, give us a shout at hello@intrinsic.com.

--

--