Before Reflect.construct()
or classes, it was common to implement inheritance by passing the value of this
, and letting the base constructor mutate it.
function Base() {
this.name = "Base";
}
function Extended() {
Base.call(this);
this.otherProperty = "Extended";
}
Object.setPrototypeOf(Extended.prototype, Base.prototype);
Object.setPrototypeOf(Extended, Base);
console.log(new Extended());
However, call()
and apply()
actually call the function instead of constructing it, so new.target
has value undefined
. This means that if Base()
checks whether it's constructed with new
, an error will be thrown, or it may behave in other unexpected ways. For example, you can't extend Map
this way, because the Map()
constructor cannot be called without new
.
All built-in constructors directly construct the entire prototype chain of the new instance by reading new.target.prototype
. So to make sure that (1) Base
is constructed with new
, and (2) new.target
points to the subclass instead of Base
itself, we need to use Reflect.construct()
.
function BetterMap(entries) {
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"));
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.
However, if you are writing ES6 code, prefer using classes and extends
instead, as it's more readable and less error-prone.
class BetterMap extends Map {
upsert(key, actions) {
if (this.has(key)) {
this.set(key, actions.update(this.get(key)));
} else {
this.set(key, actions.insert());
}
}
}
const map = new BetterMap([["a", 1]]);
map.upsert("a", {
update: (value) => value + 1,
insert: () => 1,
});
console.log(map.get("a"));