Перевірка на рівність та однаковість

У ES2015 існує чотири алгоритми рівності:

  • Абстрактна рівність (==);
  • Строга рівність (===): вживається у методах Array.prototype.indexOf, Array.prototype.lastIndexOf та у блоках case;
  • SameValueZero (однакове значення або нуль): вживається конструкторами типізованих масивів та ArrayBuffer, так само, як і операціями з Map та Set, а також методами String.prototype.includes та Array.prototype.includes, починаючи з ES2016;
  • SameValue (однакове значення): вживається у решті випадків.

JavaScript надає три різні операції порівняння значень:

  • === - Строга рівність ("ідентичність", "потрійне дорівнює")
  • == - Абстрактна рівність ("нестрога рівність", "подвійне дорівнює")
  • метод Object.is реалізує SameValue (нове у ES2015).

Вибір оператора залежитиме від того, який різновид порівняння вам потрібен. Коротко:

  • подвійне дорівнює (==) виконає приведення типів при порівнянні двох величин, і оброблятиме NaN-0 та +0 особливим чином, у відповідності з IEEE 754 (тому NaN != NaN, а -0 == +0);
  • потрійне дорівнює (===) виконує таке саме порівняння, як і подвійне дорівнює (і так само поводиться з NaN, -0 та +0), але без приведення типів; якщо типи величин відрізняються, повертається false.
  • Object.is не виконує приведення типів та не обробляє NaN, -0 та +0 особливим чином (має таку саму поведінку, як і ===, за винятком цих спеціальних числових значень).

Зауважте, що усі відмінності операторів стосуються їхнього поводження з простими величинами; жоден з них не порівнює, чи є параметри концептуально однаковими за структурою. Для будь-яких не примітивних об'єктів x та y, що мають однакову структуру, але є двома окремими об'єктами, всі вищенаведені форми порівняння вертатимуть false.

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

console.log('NaN != NaN = '+ (NaN != NaN));   // NaN != NaN  = true
console.log('0 == 0 ? = '+ (0 == 0));         // 0 == 0 ?    = true
console.log('0 === 0 ? = '+ (0 === 0));       // 0 === 0 ?   = true
console.log('+0 == -0 ? = '+ (+0 == -0));     // +0 == -0 ?  = true
console.log('+0 === -0 ? = '+ (+0 === -0));   // +0 === -0 ? = true

Строга рівність за допомогою ===

Строга рівність перевіряє рівність двох значень. До жодного з них не застосовується неявне приведення перед порівнянням. Якщо значення мають різні типи, вони вважаються нерівними. Якщо значення мають однаковий тип, не є числами, і мають однакові значення, вони вважаються рівними. Нарешті, якщо обидва значення є числами, вони вважаються рівними, якщо обидва не дорівнюють NaN та мають однакові значення, або якщо одне дорівнює +0, а інше -0.

var num = 0;
var obj = new String('0');
var str = '0';

console.log(num === num); // true
console.log(obj === obj); // true
console.log(str === str); // true

console.log(num === obj); // false
console.log(num === str); // false
console.log(obj === str); // false
console.log(null === undefined); // false
console.log(obj === null); // false
console.log(obj === undefined); // false

Строга рівність майже завжди є доречною операцією порівняння. Для усіх значень, крім чисел, вона використовує очевидну семантику: значення дорівнює лише самому собі. Для чисел вона використовує трохи відмінну семантику, щоб згладити два граничні випадки. Перший полягає в тому, що нуль з рухомою комою є або додатним, або від'ємним. Це корисно для представлення деяких математичних рішень, але, оскільки у більшості ситуацій різниця між +0 та -0 не має значення, строга рівність вважає їх одним значенням. Другий випадок полягає в тому, що рухома кома містить концепцію не числа, NaN (not a number), для вирішення деяких нечітко визначених математичних проблем: наприклад, від'ємна нескінченність, додана до позитивної нескінченності. Строга рівність вважає значення NaN нерівним будь-якій іншій величині, в тому числі самому собі. (Єдиний випадок, у якому (x !== x) дорівнює true, це коли x дорівнює NaN.)

Нестрога рівність за допомогою ==

Нестрога рівність порівнює два значення після приведення обох значень до спільного типу. Після приведення (один чи обидва значення можуть зазнати перетворення), фінальне порівняння виконується так само, як його виконує оператор ===. Нестрога рівність є симетричною: вираз A == B за семантикою завжди ідентичний B == A для будь-яких значень A та B (за винятком порядку, в якому виконуються перетворення).

Порівняльна операція виконується наступним чином для операндів різних типів:

Операнд B
Undefined Null Number String Boolean Object
Операнд A Undefined true true false false false false
Null true true false false false false
Number false false A === B A === ToNumber(B) A === ToNumber(B) A == ToPrimitive(B)
String false false ToNumber(A) === B A === B ToNumber(A) === ToNumber(B) A == ToPrimitive(B)
Boolean false false ToNumber(A) === B ToNumber(A) === ToNumber(B) A === B ToNumber(A) == ToPrimitive(B)
Object false false ToPrimitive(A) == B ToPrimitive(A) == B ToPrimitive(A) == ToNumber(B) A === B

У наведеній вище таблиці ToNumber(A) пробує перетворити свій аргумент на число перед порівнянням. Його поведінка еквівалентна операції +A (унарний оператор +). ToPrimitive(A) пробує перетворити свій аргумент-об'єкт на просту величину, викликаючи в різній послідовності методи A.toString та A.valueOf на операнді A.

Традиційно, та згідно з ECMAScript, усі об'єкти нестрого нерівні undefined та null. Але більшість переглядачів дозволяють дуже вузькому класу об'єктів (зокрема, об'єкту document.all на будь-якій сторінці) у певному контексті поводитись, як наче вони емулюють значення undefined. Нестрога рівність у такому контексті: null == A та undefined == A оцінить як true тільки за умови, що A є об'єктом, який емулює undefined. У всіх інших випадках об'єкт ніколи не є нестрого рівним undefined або null.

var num = 0;
var obj = new String('0');
var str = '0';

console.log(num == num); // true
console.log(obj == obj); // true
console.log(str == str); // true

console.log(num == obj); // true
console.log(num == str); // true
console.log(obj == str); // true
console.log(null == undefined); // true

// обидва дорівнюють false, крім виняткових випадків
console.log(obj == null);
console.log(obj == undefined);

Деякі розробники вважають, що краще ніколи не використовувати нестрогу рівність. Результат порівняння за допомогою строгої рівності легше передбачити, а, оскільки жодного приведення типів не виконується, обчислення може відбуватись швидше.

Порівняння за алгоритмом same-value

Порівняння same-value (однакове значення) спрямоване на останній випадок використання: визначення того, чи є два значення функціонально ідентичними в усіх контекстах. (Цей випадок використання демонструє приклад принципу підстановки Лісков.) Один приклад виникає, коли робиться спроба змінити незмінну властивість:

// Додати незмінну властивість NEGATIVE_ZERO у конструктор Number.
Object.defineProperty(Number, 'NEGATIVE_ZERO',
                      { value: -0, writable: false, configurable: false, enumerable: false });

function attemptMutation(v) {
  Object.defineProperty(Number, 'NEGATIVE_ZERO', { value: v });
}

Object.defineProperty викине виняток, коли спроба змінити незмінну властивість дійсно змінить її, але нічого не зробить, якщо не робиться запитів на реальну зміну. Якщо v дорівнює -0, запитів на зміну не виконувалось, жодна помилка не буде викинута. Внутрішньо, коли перевизначається незмінна властивість, нове значення порівнюється з наявним значенням за допомогою алгоритму same-value.

Алгоритм same-value надається методом Object.is.

Порівняння за алгоритмом same-value-zero

Схожий на алгоритм same-value, але вважає +0 та -0 рівними.

Абстрактна рівність, строга рівність та однакове значення у специфікації

У ES5 порівняння, що виконується за допомогою ==, описане у Розділі 11.9.3, Алгоритм абстрактної рівності. Порівняння === у 11.9.6, Алгоритм строгої рівності. (Сходіть почитайте. Вони короткі та легкі для читання. Підказка: читайте спочатку алгоритм строгої рівності.) ES5 також описує, у Розділі 9.12, Алгоритм SameValue, для внутрішнього використання рушієм JS. Він значною мірою такий самий, як алгоритм строгої рівності, за винятком того, як 11.9.6.4 та 9.12.4 відрізняються у поводженні з числами. ES2015 просто відкриває цей алгоритм через Object.is.

Щодо подвійного та потрійного дорівнює, можна побачити, що, за винятком попередньої перевірки типу у 11.9.6.1, алгоритм строгої рівності є підмножиною алгоритму абстрактної рівності, бо 11.9.6.2–7 відповідають 11.9.3.1.a–f.

Модель для розуміння порівняльних алгоритмів?

До ES2015 ви, можливо, сказали б щодо подвійного та потрійного дорівнює, що один є "посиленою" версією іншого. Наприклад, хтось може сказати, що подвійне дорівнює є посиленою версією потрійного дорвінює, тому що перше робить усе, що робить друге, але з приведенням типів у операндах. Наприклад, 6 == "6". (Альтернативно, хтось може сказати, що подвійне дорівнює є основою, а потрійне дорівнює є посиленою версією, тому що воно вимагає, щоб два операнди були однакового типу, і таким чином, вводить додаткове обмеження. Яка модель краща для розуміння, залежить від того, яким чином ви розглядаєте питання.)

Однак, така модель вбудованих операторів однаковості не може бути поширена на ES2015 з включенням у свій "діапазон" методу Object.is. Метод Object.is не просто "абстрактніший", ніж подвійне, або "суворіший", ніж потрійне дорівнює, він також не вписується десь посередині (тобто, будучи одночасно суворішим за подвійне дорівнює, але менш строгим за потрійне дорівнює). Як можна побачити з наведеної нижче порівняльної таблиці, все через поводження Object.is з NaN. Зверніть увагу, що, якби значення Object.is(NaN, NaN) дорівнювало false, ми могли б сказати, що метод вписується у абстрактно-суворий діапазон як ще суворіший за потрійне дорівнює, такий, що розрізняє -0 та +0. Однак, його поводження з NaN означає, що це не так. На жаль, Object.is просто має сприйматись з точки зору його особливих характеристик, а не його абстрактності чи суворості у порівнянні з операторами рівності.

Порівняльні алгоритми
x y == === Object.is SameValueZero
undefined undefined true true true true
null null true true true true
true true true true true true
false false true true true true
'foo' 'foo' true true true true
0 0 true true true true
+0 -0 true true false true
+0 0 true true true true
-0 0 true true false true
0 false true false false false
"" false true false false false
"" 0 true false false false
'0' 0 true false false false
'17' 17 true false false false
[1, 2] '1,2' true false false false
new String('foo') 'foo' true false false false
null undefined true false false false
null false false false false false
undefined false false false false false
{ foo: 'bar' } { foo: 'bar' } false false false false
new String('foo') new String('foo') false false false false
0 null false false false false
0 NaN false false false false
'foo' NaN false false false false
NaN NaN false false true true

Коли використовувати Object.is та потрійне дорівнює

Загалом, єдиний випадок, коли особлива поведінка Object.is щодо нулів може становити інтерес, це впровадження певних схем метапрограмування, особливо тих, що стосуються дескрипторів властивостей, коли бажано, щоб ваша робота відображала певні характеристики Object.defineProperty. Якщо ваш випадок цього не вимагає, пропонується уникати Object.is та використовувати натомість ===. Навіть якщо у вашому випадку вимагається, щоб порівняння двох NaN повертало true, загалом, легше перевіряти NaN окремо (за допомогою методу isNaN, доступного у попередніх версіях ECMAScript), ніж розбиратися, як навколишні обчислення можуть вплинути на знаки нулів, які зустрінуться вам у порівняннях.

Ось невичерпний список вбудованих методів та операцій, що можуть спричинити відмінності між -0 та +0, які можуть проявити себе у коді:

- (унарний мінус)
let stoppingForce = obj.mass * -obj.velocity;

Якщо obj.velocity дорівнює 0 (або обчислюється як 0), в цьому місці отримуємо -0, який переходить далі у stoppingForce.

Math.atan2
Math.ceil
Math.pow
Math.round
У деяких випадках -0 може потрапити у вираз як результат одного з цих методів, навіть коли жоден параметр не дорівнює -0. Наприклад, при використанні Math.pow для піднесення -Infinity до будь-якого від'ємного непарного степеня, отримуємо -0. Звертайтесь до документації щодо окремих методів.
Math.floor
Math.max
Math.min
Math.sin
Math.sqrt
Math.tan
Можливо отримати -0 як результат виконання даних методів у певних випадках, коли -0 присутній як один з параметрів. Наприклад, Math.min(-0, +0) повертає -0. Звертайтесь до документації щодо окремих методів.
~
<<
>>
Кожний з цих операторів внутрішньо використовує алгоритм ToInt32. Оскільки існує лише одне представлення 0 у внутрішньому 32-бітному цілочисельному типі, -0 не збережеться після подвійної операції інверсії. Наприклад, і Object.is(~~(-0), -0), і Object.is(-0 << 2 >> 2, -0) повернуть false.

Покладатися на метод Object.is, не беручи до уваги знаки нулів, може бути небезпечно. Звісно, якщо на меті стоїть розрізнити -0 та +0, він робить саме те, що потрібно.

Попередження: Object.is та NaN

Специфікація Object.is вважає усі екземпляри NaN тим самим об'єктом, але, оскільки нам доступні типізовані масиви, ми можемо мати окремі екземпляри, які не поводитимуться ідентично в усіх контекстах. Приклад:

var f2b = x => new Uint8Array(new Float64Array([x]).buffer);
var b2f = x => new Float64Array(x.buffer)[0];
var n = f2b(NaN);
n[0] = 1;
var nan2 = b2f(n);
nan2
> NaN
Object.is(nan2, NaN)
> true
f2b(NaN)
> Uint8Array(8) [0, 0, 0, 0, 0, 0, 248,127)
f2b(nan2)
> Uint8Array(8) [1, 0, 0, 0, 0, 0, 248,127)

Див. також