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:
Derived.prototype
, whose prototype must be Base.prototype
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 (likeError
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.
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.