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 beBase.prototype
- the static properties should be inherited:
Derived
's prototype should beBase
.
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.