Search code examples
javascriptecmascript-6ecmascript-5

Proper subclassing of built-ins in ES5, counterproof to MDN statement


I'm writing some in-depth educational materials about the object model, constructor functions and class mechanisms in Javascript. As I was reading sources I found this statement that I took the time to understand its reasons and ultimately, I found it not to be entirely true.

The MDN article on new.target sugests using Reflect.construct in order to subclass constructor functions (of course, it also advocates for the use of class syntax in ES6 code, but my interest now is on constructor functions).

As I see it, the requirements for proper subclassing are:

  • the base constructor must action on the constructed object first, then the derived constructor
  • the constructed object's prototype must be Derived.prototype, whose prototype must be Base.prototype
  • the static properties should be inherited: Derived's prototype should be Base.

There is also the implicit requirement that the base constructor's prechecks for new operator usage be satisfied. This is the key difference between constructor functions pre- and post-ES6: in ES5, this precheck consisted of a this instanceof Base check (although ECMAScript's 5.1 specification only describes the behaviours of built-ins with and without the new operator, while being ambiguous about how this check is performed).

The mdn article says:

Note: In fact, due to the lack of Reflect.construct(), it is not possible to properly subclass built-ins (like Error subclassing) when transpiling to pre-ES6 code.

and gives this example of properly subclassing Map in ES6 code:

function BetterMap(entries) {
  // Call the base class constructor, but setting `new.target` to the subclass,
  // so that the instance created has the correct prototype chain.
  return Reflect.construct(Map, [entries], BetterMap);
}

BetterMap.prototype.upsert = function (key, actions) {
  if (this.has(key)) {
    this.set(key, actions.update(this.get(key)));
  } else {
    this.set(key, actions.insert());
  }
};

Object.setPrototypeOf(BetterMap.prototype, Map.prototype);
Object.setPrototypeOf(BetterMap, Map);

const map = new BetterMap([["a", 1]]);
map.upsert("a", {
  update: (value) => value + 1,
  insert: () => 1,
});
console.log(map.get("a")); // 2

However, if BetterMap were defined instead as

function BetterMap(entries) {
    var instance = new Map(entries);
    Object.setPrototypeOf(instance, BetterMap.prototype);
    return instance;
}

wouldn't we achieve the same result, both in ES5 and ES6 (after also replacing the arrow functions in the example above with function expressions)? This satisfies all the above requirements and complies with any prechecks that Map might do, as it construct the object with new.

I tested the code above with my suggested changes on Node downgraded to 5.12.0 and it worked as expected.


Solution

  • Wouldn't we achieve the same result, both in ES5 and ES6?

    No, since Object.setPrototypeOf is not available in ES5. This was really the main limitation, you cannot subclass Array or Function in ES5 without relying on something like the non-standard (and now deprecated) .__proto__ setter. See this answer or How ECMAScript 5 still does not allow to subclass array for details.