JS Tip of the Day: Exotic Objects

Exotic Objects
Level: Advanced

All objects in JavaScript can be grouped into two different, high level categories: ordinary and exotic. Ordinary objects are your everyday objects with a standard, predictable behavior. Properties of these objects work exactly how you’d expect without any hidden surprises. Exotic objects, on the other hand, can be a little less predictable.

Exotic objects are those objects that don’t necessarily work entirely as expected or have a little extra “magic” to go above and beyond when it comes to normal, everyday operations. Some of the builtin object types that are exotic include:

  • Array
  • Proxy
  • String
  • Arguments
  • Module

Each of these types have something about them that make them a little different or unique when compared to ordinary objects.

If you’re familiar with arrays, you may already know what makes them exotic. Arrays are special in that they have a length property which automatically increases when indexed properties get added, and when set, can have influence over those properties.

let answers = [];
console.log(answers.length); // 0
answers[0] = 42;
console.log(answers.length); // 1
answers.length = 0;
console.log(answers[0]); // undefined

The behavior of length and indexed array elements in array objects is part of their exotic nature. These behaviors have to be handled by the runtime specifically for arrays and are not something that would get applied to other, ordinary objects.

Long time users of JavaScript may be familiar with past difficulties in trying to extend the Array type in an attempt to make an Array subclass. Doing so was problematic specifically because arrays are exotic. If you have a constructor function that creates a new instance, even if that instance will inherit from Array.prototype, the instance will not be an array exotic object with a functioning length property.

function DumbArray () {
    // closest thing to super() (Array() creates new array)
    Array.prototype.push.apply(this, arguments);
}
// extends
DumbArray.prototype = Object.create(Array.prototype);
DumbArray.prototype.constructor = DumbArray;

let dummy = new DumbArray(1,2,3);
console.log(dummy.length); // 3
dummy[3] = 4;
console.log(dummy.length); // 3

Thankfully, much like we’ve seen before with internal slots, the class syntax does allow us to properly initialize exotic types to correctly create an exotic object instance. This means we can now properly subclass Array (or potentially other exotic types) to make our own subtypes whose instances will be able to have the expected exotic object behaviors.

class SmartArray extends Array {}

let smarty = new SmartArray(1,2,3);
console.log(smarty.length); // 3
smarty[3] = 4;
console.log(smarty.length); // 4

While there are no ways to detect if an object is exotic or not, for the most part, you shouldn’t need to. Most exotic objects do not have behaviors so “exotic” that they can’t be, for the most part, treated like normal objects. Any differences should be explained through documentation (e.g. on MDN). Arrays are most likely going to be your most common exotic object that have any discernible difference in behavior, but if necessary, you can easily check for them using Array.isArray.

console.log(Array.isArray(dummy)); // false
console.log(Array.isArray(smarty)); // true

More info:

1 Like