28 juin 2023

Vérification de classe : "instanceof"

L’opérateur instanceof permet de vérifier si un objet appartient à une certaine classe. Il prend également en compte l’héritage.

Une telle vérification peut être nécessaire dans de nombreux cas. Nous l’utilisons ici pour construire une fonction polymorphique, celle qui traite les arguments différemment en fonction de leur type.

L'opérateur instanceof

La syntaxe est la suivante :

obj instanceof Class

Cela renvoie true si obj appartient à la Class ou à une classe qui en hérite.

Par exemple :

class Rabbit {}
let rabbit = new Rabbit();

// est-ce un objet de la classe Rabbit ?
alert( rabbit instanceof Rabbit ); // true

Cela fonctionne aussi avec les fonctions constructeur :

// au lieu de classe
function Rabbit() {}

alert( new Rabbit() instanceof Rabbit ); // true

…Et avec des classes intégrées comme Array :

let arr = [1, 2, 3];
alert( arr instanceof Array ); // true
alert( arr instanceof Object ); // true

Veuillez noter que arr appartient également à la classe Object. C’est parce que Array hérite de manière prototypale de Object.

Normalement, l’opérateur instanceof examine la chaîne prototypale pour la vérification. Nous pouvons également définir une logique personnalisée dans la méthode statique Symbol.hasInstance.

L’algorithme de obj instanceof Class fonctionne à peu près comme suit :

  1. S’il existe une méthode statique Symbol.hasInstance, appelez-la simplement : Class[Symbol.hasInstance](obj). Cela devrait renvoyer true ou false, et nous avons terminé. C’est ainsi que nous pouvons personnaliser le comportement de instanceof.

Par exemple :

// configuration du contrôle de instanceof qui suppose que
// tout ce qui a la propriété canEat est un animal
class Animal {
  static [Symbol.hasInstance](obj) {
    if (obj.canEat) return true;
  }
}

let obj = { canEat: true };

alert(obj instanceof Animal); // true: Animal[Symbol.hasInstance](obj) est appelée
  1. La plupart des classes n’ont pas Symbol.hasInstance. Dans ce cas, la logique standard est utilisée : obj instanceof Class vérifie si Class.prototype est égale à l’un des prototypes de la chaîne prototypale obj.

    En d’autres termes, on compare l’un après l’autre :

obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
...
// si une réponse est vraie, renvoie true
// sinon, si nous arrivons au bout de la chaîne, renvoie false
Dans l'exemple ci-dessus, `rabbit.__ proto__ === Rabbit.prototype`, donne donc la réponse immédiatement.

Dans le cas d'un héritage, la correspondance se fera à la deuxième étape :
class Animal {}
class Rabbit extends Animal {}

let rabbit = new Rabbit();
alert(rabbit instanceof Animal); // true

// rabbit.__proto__ === Animal.prototype (pas de correspondance)
// rabbit.__proto__.__proto__ === Animal.prototype (ça correspond!)

Voici l’illustration de ce que rabbit instanceof Animal compare avec Animal.prototype :

À propos, il y a aussi une méthode objA.isPrototypeOf(objB), qui renvoie true si objA se trouve quelque part dans la chaîne de prototypes pour objB. Ainsi, le test de obj instanceof Class peut être reformulé comme suit : Class.prototype.isPrototypeOf(obj).

C’est drôle, mais le constructeur Class lui-même ne participe pas au contrôle ! Seule la chaîne de prototypes et Class.prototype compte.

Cela peut avoir des conséquences intéressantes lorsqu’une propriété prototype est modifiée après la création de l’objet.

Comme ici :

function Rabbit() {}
let rabbit = new Rabbit();

// le prototype est changé
Rabbit.prototype = {};

// ...plus un rabbit !
alert( rabbit instanceof Rabbit ); // false

Bonus : Object.prototype.toString pour le type

Nous savons déjà que les objets simples sont convertis en chaîne sous la forme [objet Objet] :

let obj = {};

alert(obj); // [object Object]
alert(obj.toString()); // la même chose

C’est leur implémentation de toString. Mais il existe une fonctionnalité cachée qui rend toString beaucoup plus puissant que cela. Nous pouvons l’utiliser comme un typeof étendu et une alternative pour instanceof.

Cela semble étrange ? Effectivement. Démystifions.

Par spécification, le toString intégré peut être extrait de l’objet et exécuté dans le contexte de toute autre valeur. Et son résultat dépend de cette valeur.

  • Pour un nombre, ce sera [object Number]
  • Pour un booléen, ce sera [object Boolean]
  • Pour null : [objet Null]
  • Pour undefined : [objet Undefined]
  • Pour les tableaux : [objet Array]
  • … etc. (personnalisable).

Montrons cela :

// copier la méthode toString dans une variable pour plus d'utilité
let objectToString = Object.prototype.toString;

// quel type est-ce ?
let arr = [];

alert( objectToString.call(arr) ); // [object Array]

Ici nous avons utilisé call comme décrit dans le chapitre Décorateurs et transferts, call/apply pour exécuter la fonction objectToString dans le contexte this=arr.

En interne, l’algorithme toString examine this et renvoie le résultat correspondant. Plus d’exemples :

let s = Object.prototype.toString;

alert( s.call(123) ); // [object Number]
alert( s.call(null) ); // [object Null]
alert( s.call(alert) ); // [object Function]

Symbol.toStringTag

Le comportement de Object toString peut être personnalisé à l’aide d’une propriété d’objet spéciale Symbol.toStringTag.

Par exemple :

let user = {
  [Symbol.toStringTag]: "User"
};

alert( {}.toString.call(user) ); // [object User]

Pour la plupart des objets spécifiques à l’environnement, il existe une telle propriété. Voici quelques exemples spécifiques à votre navigateur :

// toStringTag pour l'objet et la classe spécifiques à l'environnement :
alert( window[Symbol.toStringTag]); // Window
alert( XMLHttpRequest.prototype[Symbol.toStringTag] ); // XMLHttpRequest

alert( {}.toString.call(window) ); // [object Window]
alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest]

Comme vous pouvez le constater, le résultat est exactement Symbol.toStringTag (s’il existe), encapsulé dans [objet ...].

Au final, nous avons un “typeof sous stéroïdes” qui fonctionne non seulement pour les types de données primitifs, mais aussi pour les objets intégrés et qui peut même être personnalisé.

Nous pouvons utiliser {}.toString.call au lieu de instanceof pour les objets intégrés lorsque nous voulons obtenir le type sous forme de chaîne de caractères plutôt que pour simplement vérifier.

Résumé

Résumons les méthodes de vérification de type que nous connaissons :

fonctionne pour renvoie
typeof primitives string
{}.toString primitives, objets intégrés, objets avec Symbol.toStringTag string
instanceof objects true/false

Comme on peut le constater, {}.toString est techniquement un typeof “plus avancé”.

Et l’opérateur instanceof excelle lorsque nous travaillons avec une hiérarchie de classes et voulons vérifier si la classe prend en compte l’héritage.

Exercices

importance: 5

Dans le code ci-dessous, pourquoi instanceof renvoie true ? Nous pouvons facilement voir que a n’est pas créé par B().

function A() {}
function B() {}

A.prototype = B.prototype = {};

let a = new A();

alert( a instanceof B ); // true

Ouais, ça a l’air étrange.

Mais instanceof ne se soucie pas de la fonction, mais plutôt de son prototype, qui correspond à la chaîne de prototypes.

Et ici a.__ proto__ == B.prototype, ainsi instanceof renvoie true.

Ainsi, par la logique de instanceof, le prototype définit en fait le type, pas la fonction constructeur.

Carte du tutoriel