Accueil / Articles

Explication de l’égalité en JavaScript : ===, ==, opérateurs relationnels et Object.is

Découvrez comment l’égalité stricte et lue, les comparaisons relationnelles ainsi que Object.is déterminent réellement la similarité en JavaScript, ainsi que les pièges de coercition que chacun d’eux cache.

Éditeur : REACTAPP.TOP10 min de lecture2405 mots

Sources: Day 4 of Learning JavaScript: The Three Ways JavaScript Asks “Are These the Same Thing?”

Imaginez un formulaire où l’utilisateur saisisse son âge sous la forme du texte "18", et où une vérification contre le nombre 18 passe silencieusement, pour que des calculs ultérieurs sur cette valeur échouent par la suite. Ou encore une vérification d’accès qui réussit alors qu’elle ne devrait pas, parce que deux valeurs différentes se sont éventuellement transformées en la même chose. De tels bugs proviennent d’un fait : JavaScript dispose de plusieurs moyens de déterminer si deux valeurs sont identiques, et chacun définit le concept d’« identique » de manière différente. Après avoir lu ceci, vous saurez ce que font réellement ===, ==, les opérateurs relationnels et Object.is(), ainsi que lequel choisir dans chaque situation.

Pourquoi le langage possède plusieurs notions d’égalité

Le JavaScript initial fonctionnait dans des navigateurs où presque toutes les entrées arrivaient sous forme de texte provenant d’un formulaire. Plutôt que de générer une erreur chaque fois qu’un nombre apparaissait en tant que chaîne, le langage a été conçu pour être tolérant : il devinait ce que l’on voulait dire et convertissait automatiquement les types. Cette commodité est devenue l’une des sources les plus fréquentes d’erreurs dans ce langage, et les opérateurs plus stricts existent principalement pour y remédier.

L’égalité stricte (===) par défaut

Vérification du type suivie d’une vérification de la valeur

L’égalité stricte effectue un test en deux étapes sans conversion cachée. D’abord, elle compare les types des deux opérandes. S’ils diffèrent, la réponse est false sans que les valeurs ne soient jamais examinées. Ce n’est que lorsque les types correspondent-ils qu’elle compare les valeurs.

console.log(1 === 1);
// true (both are numbers, both are 1)

console.log(1 === "1");
// false (the types fail immediately: number versus string)

console.log(0 === false);
// false (number versus boolean)

console.log("" === false);
// false (string versus boolean)

console.log(null === null);
// true

console.log(null === undefined);
// false (different types)

Tout false dans cet extrait provient de la comparaison entre types : nombre contre chaîne, nombre contre booléen, chaîne contre booléen, null contre undefined.

Les comparaisons de chaînes sont exactes au niveau des caractères. La capitalisation compte, tout comme les espaces invisibles, ce qui est une cause très fréquente d’erreurs dans la vie réelle lorsque quelqu’un appuie sur la touche espace après avoir saisi une adresse e-mail.

console.log("hello" === "Hello");
// false (different capitalization)

console.log("john@email.com" === "john@email.com ");
// false (the second string has a hidden space at the end)

En pratique, cela signifie qu’il faut normaliser les entrées utilisateur, généralement avec trim() et souvent toLowerCase() pour les champs insensibles à la casse, avant de les comparer.

Deux exceptions délibérées du standard IEEE 754

L’égalité stricte est fiable presque partout, à deux exceptions numériques près. Aucune de ces exceptions n’est un bug ; elles découlent toutes deux de la norme IEEE 754 pour les nombres à virgule flottante sur laquelle sont basés les nombres JavaScript. La première concerne NaN, la valeur « pas de nombre », qui n’est égale à rien, y compris à elle-même.

const result = 0 / 0;
// result is NaN

console.log(result === NaN);
// false

console.log(NaN === NaN);
// false

Puisqu’un test d’égalité ne peut jamais la détecter, utilisez plutôt la fonction dédiée Number.isNaN().

console.log(Number.isNaN(NaN));
// true

Préférez-la à l’ancienne fonction globale isNaN(), qui convertit d’abord son argument en nombre et rapporte donc true pour des chaînes comme "abc". La deuxième exception concerne zéro : JavaScript représente séparément le zéro positif et négatif, ce qui est important dans certains codes numériques, mais l’égalité stricte les considère comme égaux.

console.log(+0 === -0);
// true

Les objets sont comparés par identité

Les primitives telles que les nombres et les chaînes de caractères sont comparées par leur valeur. Les objets sont différents : une variable qui contient un objet ne possède en réalité qu’une référence à l’endroit où cet objet se trouve en mémoire. Imaginez deux maisons construites selon les mêmes plans et peintes de la même couleur, situées dans des rues différentes. Elles ont l’air identiques, mais ce ne sont pas la même maison, car leurs adresses diffèrent.

Lorsque vous comparez des objets avec ===, JavaScript vérifie si les deux côtés font référence au même objet et ignore son contenu. Les tableaux suivent la même règle, car ce sont eux aussi des objets.

const userA = { name: "Alex" };
const userB = { name: "Alex" };

console.log(userA === userB);
// false (two different objects sitting in different memory slots)

console.log([] === []);
// false (two different arrays sitting in different memory slots)

Si une variable est assignée à partir d’une autre, les deux font référence au même objet, et la comparaison réussit.

const original = { name: "Alex" };
const pointer = original;

console.log(original === pointer);
// true (both point to the exact same memory location)

Pour vérifier si deux objets distincts contiennent les mêmes données, comparez vous-même leurs propriétés primitives.

console.log(userA.name === userB.name);
// true (now we are comparing two strings, not two object addresses)

Pour les structures imbriquées, cela se transforme en une comparaison récursive ; c’est pourquoi les équipes ont généralement recours à un outil d’égalité profonde éprouvé provenant d’une bibliothèque d’outils ou de leur framework de tests, plutôt que d’en écrire un sur mesure.

Inégalité stricte (!==)

Vérifier que les valeurs diffèrent est tout aussi courant que de vérifier qu’elles correspondent. L’inégalité stricte est la négation exacte de === : elle renvoie true lorsque les types ou les valeurs diffèrent.

console.log(1 !== 2);
// true (they are different numbers)

console.log(1 !== "1");
// true (one is a number, one is a string)

console.log("apple" !== "orange");
// true

C’est l’option naturelle chaque fois que le code doit s’exécuter uniquement si une condition n’est pas remplie, comme pour les vérifications de permissions ou le filtrage des éléments indésirables.

const currentRole = "guest";

if (currentRole !== "admin") {
  console.log("Access denied. Admins only.");
}
// "Access denied. Admins only."

Égalité lâche (==) et ses conversions

Comment la coercition produit des résultats surprenants

Au moment où l’égalité stricte n’était pas encore la norme, la plupart des codes utilisaient ==. Lorsque les types des opérandes diffèrent, l’égalité lâche convertit l’un ou les deux valeurs en un type commun avant de les comparer. Les résultats ne correspondent plus rapidement à l’intuition.

console.log(1 == "1");
// true (the string "1" becomes the number 1)

console.log(0 == false);
// true (false becomes 0)

console.log("" == false);
// true (empty string and false both become 0)

console.log(0 == "");
// true (empty string becomes 0)

console.log([] == "");
// true (empty array becomes empty string)

console.log([1] == 1);
// true (array becomes "1", which becomes 1)

Un exemple célèbre montre combien d’étapes cachées peuvent être impliquées.

console.log([] == 0);
// true

Pour obtenir true, le moteur convertit d’abord le tableau vide en une valeur primitive, ce qui donne la chaîne de caractères vide "". Les types diffèrent toujours, car une chaîne est comparée à un nombre ; par conséquent, la chaîne est convertie en nombre, donnant 0. La comparaison finale est 0 == 0, ce qui équivaut à true.

Les valeurs booléennes présentent également des difficultés, car lorsqu’elles sont comparées à une valeur non booléenne, elles sont d’abord converties en nombres avant toute autre opération. true devient 1.

console.log(true == "true");
// false

La chaîne "true" ne devient pas 1 ; convertie en nombre, elle devient NaN, et 1 n’est pas égal à NaN. C’est pourquoi value == true n’est presque jamais ce que l’on souhaite.

null et undefined suivent une règle particulière : avec l’égalité lâche, ils sont égaux l’un à l’autre et à rien d’autre, même pas à 0, false ou à la chaîne vide.

console.log(null == undefined);
// true

console.log(null == 0);
// false

console.log(null == false);
// false

console.log(null == "");
// false

console.log(undefined == 0);
// false

Vérification de null où l’égalité lâche est justifiée

L’égalité lâche doit être évitée autant que possible dans le code moderne, avec une exception largement utilisée. Lors du traitement de données provenant d’une API, on souhaite souvent savoir si une valeur est null ou undefined, et == null permet de répondre à cette question en une seule comparaison.

function checkStatus(status) {
  if (status == null) {
    console.log("Status is missing entirely");
  } else {
    console.log("Status is:", status);
  }
}

checkStatus(null);
// "Status is missing entirely"

checkStatus(undefined);
// "Status is missing entirely"

checkStatus(0);
// "Status is: 0"

Cela fonctionne précisément en raison de la règle mentionnée ci-dessus : null n’est égal que de manière lâche à null et undefined ; ainsi, des valeurs fausses mais significatives comme 0 ou "" passent intactes. Cela remplace l’expression plus longue status === null || status === undefined. Notez que de nombreuses équipes configurent leurs outils de vérification pour interdire complètement l’opérateur ==, auquel cas il faut utiliser la version stricte explicite. Par exemple, la règle eqeqeq d’ESLint dispose d’une option qui autorise précisément cette comparaison avec null tout en interdisant ses autres utilisations. Dans de nombreux cas, l’opérateur de fusion des valeurs nulles ?? ou la chaînage optionnel ?. expriment l’intention de manière encore plus directe.

Inégalité lâche (!=)

!= applique les mêmes conversions que == avant de nier le résultat, ce qui lui fait subir tous les problèmes associés.

console.log(1 != "1");
// false (it converts "1" to a number, decides they are the same, so they are not different)

Ici, "1" est converti en le nombre 1 ; les valeurs sont considérées comme égales, et la vérification de « non égalité » renvoie donc false. Traitez != avec la même méfiance que == et préférez !==.

Comparaisons relationnelles : <, >, <= et >=

Nombres et types mixtes

Avec deux nombres, les opérateurs relationnels se comportent exactement comme en arithmétique.

console.log(5 > 3);
// true

console.log(10 <= 10);
// true

Lorsqu’une chaîne de caractères rencontre un nombre, la chaîne est d’abord convertie en nombre. Une chaîne ne représentant pas un nombre devient NaN, et toute comparaison relationnelle impliquant NaN donne false.

console.log("10" > 5);
// true

console.log("abc" > 3);
// false ("abc" becomes NaN, and NaN comparisons always return false)

Où null et undefined diffèrent

Les opérateurs relationnels révèlent une incohérence avec l’égalité lâche. == refuse de convertir null en nombre, mais <, >, <= et >= le convertissent en 0.

console.log(null == 0);
// false

console.log(null > 0);
// false (0 is not greater than 0)

console.log(null >= 0);
// true (0 is greater than or equal to 0)

Ainsi, null n’est ni égal à 0 ni supérieur à 0, pourtant il est supérieur ou égal à 0. Ce dernier résultat prend sens une fois que l’on sait que a >= b est évalué comme « a < b n’est pas vrai », et que 0 < 0 est faux.

undefined se comporte différemment. Converti en nombre, il devient NaN, de sorte que toute comparaison relationnelle impliquant undefined donne false.

console.log(undefined > 0);
// false

console.log(undefined < 0);
// false

Les chaînes de caractères sont comparées par unité de code, et non par ordre alphabétique

Lorsque les deux opérandes sont des chaînes de caractères, aucune conversion numérique n’a lieu. Les chaînes sont comparées caractère par caractère à partir de la gauche, en utilisant l’unité de code UTF-16 numérique de chaque caractère.

console.log("apple" < "banana");
// true (a comes before b)

console.log("b" > "a");
// true (b comes after a)

Les lettres majuscules possèdent des unités de code plus faibles que les lettres minuscules, ce qui explique pourquoi un tri naïf peut placer « Zebra » avant « apple ».

console.log("Z" < "a");
// true (Z=90, a=97 in Unicode)

Pour un tri adapté à l’usage humain, utilisez localeCompare() ou Intl.Collator, qui respectent les règles linguistiques plutôt que des unités de code brutes.

Le piège le plus dangereux concerne les chaînes contenant des chiffres. Lorsqu’elles sont comparées en tant que chaînes, « 10 » est considéré comme plus petit que « 9 ».

console.log("10" < "9");
// true (the character "1" is smaller than "9")

JavaScript ne sait pas que ces chaînes représentent des quantités. Il examine le premier caractère de chacune, "1" face à "9", constate que "1" vient en premier, et s’arrête là avec true. Le même phénomène se produit lorsqu’un tableau de nombres est trié avec sort() sans comparateur, car le tri par défaut convertit les éléments en chaînes avant de les comparer ; il faut passer (a, b) => a - b pour effectuer un tri numérique.

La solution consiste à convertir explicitement les chaînes numériques en nombres avant de comparer leurs valeurs.

console.log(Number("10") < Number("9"));
// false (correct: 10 is not less than 9)

Object.is() pour une identité exacte

Compléter les lacunes de ===

Object.is() est la vérification d’égalité la plus précise offerte par JavaScript. Pour les valeurs ordinaires, il donne les mêmes résultats que l’égalité stricte.

console.log(Object.is(1, 1));
// true

console.log(Object.is(1, "1"));
// false

Leur différence réside précisément dans les deux cas prévus par IEEE 754. Object.is() considère NaN comme étant identique à NaN:

console.log(Object.is(NaN, NaN));
// true

et il distingue zéro positif de zéro négatif :

console.log(Object.is(0, -0));
// false

Les trois vérifications côte à côte

  • 1 et "1" : == renvoie true ; === et Object.is() renvoient false.
  • 0 et false : == renvoie true ; === et Object.is() renvoient false.
  • null et undefined : == renvoie true ; === et Object.is() renvoient false.
  • NaN et NaN : == et === renvoient false ; Object.is() renvoie true.
  • +0 et -0 : == et === renvoient true ; Object.is() renvoie false.
  • [] et [], ou {} et {} : les trois opérateurs renvoient false, car il s’agit d’objets distincts.
  • 5 et 5 : les trois opérateurs renvoient true.
  • Vous appellerez rarement directement Object.is(), mais il joue un rôle important dans les outils que vous utilisez tous les jours. React s’appuie sur Object.is() pour déterminer si l’état ou une dépendance a changé et s’il est nécessaire de réafficher l’écran, c’est aussi pourquoi modifier un objet en place ne déclenche pas de mise à jour : la référence reste identique. Les bibliothèques d’état appliquent la même logique ; consultez comment React-Redux utilise des vérifications d’égalité pour décider des réaffichages pour en savoir plus.

    Points clés

    Dans le premier scénario, où une vérification aurait dû refuser l’accès mais l’a accordé, il n’est pas nécessaire d’utiliser quelque chose de plus complexe que == pour comparer une chaîne de caractères à un nombre : une conversion silencieuse fait apparaître deux valeurs différentes comme égales. Les règles qui empêchent cela sont simples :

    1. Utilisez par défaut === et !== partout.
    2. Évitez == et !=, sauf pour l’abréviation intentionnelle value == null si vos règles de linting le permettent.
    3. Détectez NaN avec Number.isNaN(), jamais à l’aide d’un opérateur d’égalité.
    4. N’oubliez pas que les comparaisons d’objets vérifient l’identité, et non le contenu.
    5. Convertissez les chaînes numériques avec Number() avant d’utiliser < ou >, et fournissez à sort() un comparateur adapté aux nombres.
    6. Préférez Object.is() lorsque NaN ou zéro signé doit être traité de manière précise.

    Les conversions tolérantes de JavaScript ne disparaîtront pas, mais en privilégiant des comparaisons strictes, on évite qu’elles influencent le comportement en production.

    Lectures complémentaires