Simulating an abstract class with new.target
Published on 21 May 2020
• 2 min
Cette page est également disponible en français.
This is nearing the end of our daily series “19 nuggets of vanilla JS.” Today we’ll look at the seldom-known new.target
, that showed up with ES2015 and lets us simulate abstract classes, among other things…
The series of 19
Check out surrounding posts from the series:
- Converting an object to
Map
and vice-versa - The
for
-of
loop: should there remain only one… - Simulating an abstract class with
new.target
(this post) - Negative array indices thanks to proxies
Wait a minute — isn’t new
an operator?!
Yes it is. Still, JS has a few syntactical oddballs, including new.target
, that is legal (inside functions) and references the operand that was passed to new
when instantiating the current object.
Consequently, if the current function is not run in the context of an object (if there is no valid resolution of this
), it will evaluate to undefined
.
Function Environment Record
Every execution of a function spawns a function environment record (FER), that lists all the accessible bindings (associations of values to identifiers) made available by the call to that function. At any time, evaluating a reference traverses a series of active environment records, that roughly align with the relevant function scopes.
The FER for a traditional (non-arrow) function includes four specific bindings dynamically defined at call time: this
, arguments
, super
and new.target
. (An arrow function has none of these, its code will therefore resolve these references in the FER of the closest enclosing non-arrow function.)
What is it for?
A common scenario is about abstract classes. Quick reminder: in OOP, an abstract class acts as the starting point of a hierarchy of classes, but is incomplete in itself: you’re not supposed to instantiate it directly.
Let’s illustrate this with the done-do-death hierarchy of geometrical shapes. Sure, they all have a few things in common, like an origin point and methods for drawing and computing the perimeter or area, that are justification enough for a common base class Shape
. But a “shape” in itself does not tell us which specific shape to draw, so instantiating new Shape(…)
wouldn’t make sense!
JavaScript has no abstract
keyword to express this, but we can simulate it by testing, in our constructor, that the operand passed to new
wasn’t the base class (i.e. Shape
) but rather a subclass (e.g. Square
):
class Shape {
constructor(origin) {
if (new.target === Shape) {
throw new Error('Cannot instantiate Shape directly!')
}
this.origin = origin
}
draw() {
throw new Error('Must override draw!')
}
}
class Square extends Shape {
constructor(origin, size) {
super(origin)
this.size = size
}
draw() {
// …
}
}
new Shape([15, 15])
// => Error: Cannot instantiate Shape directly!
new Square([15, 15], 10).draw()
// => No worries
Where can I get that?!
It’s been natively supported for a while already: Chrome 43, Firefox 41, Opera 33, Edge 13, Safari 11 and Node 5.
Babel and TypeScript transpile as always.