У 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
.
Строга рівність за допомогою ===
Строга рівність перевіряє рівність двох значень. До жодного з них не застосовується неявне приведення перед порівнянням. Якщо значення мають різні типи, вони вважаються нерівними. Якщо значення мають однаковий тип, не є числами, і мають однакові значення, вони вважаються рівними. Нарешті, якщо обидва значення є числами, вони вважаються рівними, якщо обидва не дорівнюють 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
.
- У деяких випадках
-0
може потрапити у вираз як результат одного з цих методів, навіть коли жоден параметр не дорівнює-0
. Наприклад, при використанніMath.pow
для піднесення-Infinity
до будь-якого від'ємного непарного степеня, отримуємо-0
. Звертайтесь до документації щодо окремих методів.
- Можливо отримати
-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)