Simuler une classe abstraite avec new.target
Par Christophe Porteneuve • Publié le 21 mai 2020
• 2 min
This page is also available in English.
Déjà l‘avant-dernier article de notre série quotidienne « 19 pépites de JS pur ». Aujourd’hui on parle du bien peu connu new.target
, apparu en ES2015, qui nous permet par exemple de simuler des classes abstraites…
Dans la série…
Extrait de la liste des articles :
- Convertir un objet en
Map
et réciproquement - La boucle
for
-of
: s’il ne devait en rester qu’une… - Simuler une classe abstraite avec
new.target
(cet article) - Des indices de tableaux négatifs grâce aux proxies
Attends, new
c’est pas un opérateur ?!
Mais si. Il n’empêche, JS a quelques cas très particuliers de syntaxe, et notamment new.target
, qui est légale (au sein des fonctions) et désigne l’opérande passé à new
lors de l’instanciation de l’objet courant.
Évidemment, si la fonction courante n’est pas exécutée dans le contexte d’un objet (s’il n’y a pas de résolution possible de this
), ça vaudra undefined
.
Function Environment Record
Chaque exécution de fonction est dotée d’un function environment record (FER), qui liste l’ensemble des bindings (associations de noms à des valeurs) accessibles en vertu de l’appel de fonction. À tout instant, l’évaluation d’une référence traverse une série d’environment records en vigueur, qui représentent peu ou prou les portées actives.
Une fonction non fléchée dispose dans son FER de quatre bindings spécifiques définis par son mode d’appel : this
, arguments
, super
et new.target
. (Une fonction fléchée n’a rien de tout ça, son code ira donc pêcher ces références dans le FER de la fonction non fléchée englobante la plus proche.)
À quoi ça sert ?
Un des scénarios les plus courants concerne les classes abstraites. Pour rappel, en POO, une classe abstraite sert de base à une hiérarchie de classes, mais est incomplète en elle-même : tu n’es pas censé·e l’instancier directement.
Prenons la hiérarchie archi-rebattue des formes géométriques ; elles ont certes toutes quelques points communs, comme un point d’origine et une méthode de dessin, qui justifient une classe de base commune Shape
. Mais une « forme » en elle-même ne nous renseigne pas sur la forme concrète à dessiner, de sorte qu’instancier new Shape(…)
n’aurait pas de sens !
On n’a pas de mot-clé abstract
implémenté pour dire ça, mais on peut le simuler en testant dans le constructeur que l’opérande passé à new
n’était pas la classe de base (c’est-à-dire Shape
) mais une sous-classe (par exemple 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()
// => Pas de souci
C’est dispo où ?
Dispo nativement à partir de Chrome 43, Fireofx 41, Opera 33, Edge 13, Safari 11 et Node 5.
Babel et TypeScript transpilent évidemment.