Паспрацаванне з абэраваннем у JavaScript: ===, ==, аперацыі стосункоў і Object.is
Дазвольце дазнацься, як строгая і вольная равнасць, аблікацыйныя порэўнанні і Object.is насправдзе выбіраюць аднаковасць у JavaScript, а таксама пра пасткі прымусовага перакантравання, якія хавае кожны з іх.
Крыніцы: Day 4 of Learning JavaScript: The Three Ways JavaScript Asks “Are These the Same Thing?”
Уявіце форму, дзе корыстнік вводзіць свой вяк у вачынку "18", і пераканальна перапалоўка з цыфрам 18 таямна праходзіць, толькі пазней арытметычныя вычысленні з гэтым значэнням пачынаюць даваць некоректныя рэзультаты. Або пераканальна перапалоўка доступу праходзіць, калі не павінна, таму што два разныя значэння випадкова ператварыліся на адно і тое ж. Такія багі вынікаюць з адной прычыны: у JavaScript існуе калькі спосабоў выявіць, чырэзныя два значэння є аднаковымі, і кожны з іх па-свойму вядомае, што значыць „аднаковы“. Пасля прачытання гэтага вы будете ведаць, што на самай працэ ===, ==, асоцыяцыйныя аператары і Object.is() на самай працэ, і каторы з іх трэба вжываць у кожной сітуацыі.
Чаму ў мове існуе больш адная канцэпцыя равенства
У ранніх версіях JavaScript ён працав у браузерах, дзе практычна ўсё введана інфармацыя падавалася як тэкст з формы. Уместо таго, каб выдаваць адказ пра абэрасію, калі число падавалася як строка, мова была спроектаваная так, каб прыняць гэта: яна сама адгадвала, што вы мелі на увазе, і автаматычна пераканвала типы. Гэта зручнасць стала аднам з найбольш надзеяных джэрел багоў у мове, і строгіясць аператараў існуе галоўная метай, каб ухіліцца ад гэтага.
Строгая роўнасць (===) як стандарт
Пераканранне типу, пасля чаго — пераканранне значэння
Строгая роўнасць адбываецца за два крокі без жадных скрытых пераканранняў. Спачатку яна парабягае типы двух аргументаў. Якщо яны разніцяюцца, адказ будзе false, і значэнняя не будуць абмовлены. Толькі калі типы збігаюцца, яна парабягае значэння.
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)
Кожны false у сэрвака кода выходзіць з крока па тип: число проты строкі, число проты булевага значэння, строка проты булевага значэння, null проты undefined.
Порэшчанні страк адбываецца точна на рэвэле, уключаючы симвалы. Значэнне мае напісанне больш/меньшасловамі, а таксама невидзимыя прабелы, якія часта станавяцца прычыной памылак у рэальных супадках, калі чалавек нажымае клавішу прабела пасля введэння адресы электронной пошты.
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)
У практыцы гэта значыць нормалізацію вводу ад користунача, зазвычай за дапамою trim(), а часта і toLowerCase() для полей, якія не чутлівыя да розліку больш/меньшасловаў, прычым перад ўпорэшчаннем.
Две навмешаныя адхіленнія ад стандарта IEEE 754
Строгая абяценасць ў большай частыні случаў є надзеяна, за двумя выключэннямі для числаў. Жадна з гэтых ситуацыяў не ёсць багам; обе спадкаваныя з стандарта IEEE 754 для числаў з плаваючай канматой, на якім адбываецца обробка числаў у JavaScript. Першым выключэнням є тое, што NaN, значэнне „не число“, не ўзроўнае нічаму, ваколькі не ўзроўнае нават самам сабе.
const result = 0 / 0;
// result is NaN
console.log(result === NaN);
// false
console.log(NaN === NaN);
// false
Паколькі пераглед абяценасці ніколі не можа яго выявіць, трэба вжываць спецыяльную функцію Number.isNaN().
console.log(Number.isNaN(NaN));
// true
Лепш вжываць яе замест старэй глобальнай функціи isNaN(), якая спачатку ператварае аргумент у число, і таму паведамляе true для строкаў, такіх як "abc". Другым выключэнням є нуль. JavaScript адражае пасяблівы і аб’ектыўны нуль окрэмна, што мае значэнне ў дзеякім числовым коде, пры тым часам строгая абяценасць счыляе іх за роўныя.
console.log(+0 === -0);
// true
Об’екты парабягаюцца па ідэнтычнасці
Прымітывы, такія як цалы і строкі, пораўнююцца па значэнні. Об’екты ж адрознены: змянная, якая храніць об’ект, на самай працэ паказвае ссылку на месца ў памяці, дзе знаходзіцься той об’ект. Уявіце два будынкі, пабудаваныя за аднымі планамі і выкрашаныя тым самым кольорам, але на разных вуліцах. Яны выглядаюць ідэнтычна, але не ўсё той самы будынак, бо у яных розныя адресы.
Калі вы пораўнюеце об’екты за дапамогою ===, JavaScript пераканальваецца, чыя бы то ні было стороны паказваюць ссылку на той самы об’ект, і ігнаруее тое, што ў яму знаходзіцца. Масівы таксама паводзяцца за тым жа правілам, адколі яны таксама ўжо є об’ектамі.
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)
Якщо адна змянная прызначаецца з іншай, якія б то ні было стороны паказваюць ссылку на той самы об’ект, і пораўненне успішна.
const original = { name: "Alex" };
const pointer = original;
console.log(original === pointer);
// true (both point to the exact same memory location)
Ёсць неабходнасць пераканацца, чы два розныя об’екты маюць тыя ж даны, трэба самі пораўняць ўсе іх прымітывныя атрыбуты.
console.log(userA.name === userB.name);
// true (now we are comparing two strings, not two object addresses)
Для вяроўнаваных структурах адбываецца рекурсыўнае парабяранне, таму команды зазвычай выкарыстоўваюць апробаваны функцыянак для глęбокай аднаковасці з бібліятэкі інструментаў або свайго фрэймворку теставання, замест таго каб пісаць яго самостайна.
Строгая неаднаковасць (!==)
Парабяранне на тое, чы розныя значэння не аднаковыя, є настолькі ж распашчастае, як і парабяранне на ўзаемную аднаковасць. Строгая неаднаковасць — это точная працоўка ===: яна вярнэе true, калі розныя типы або значэння.
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
Эта спосаб яшчэ болей падходзіць, калі код должен выконвацца толькі тады, калі паводак не выпалоўваецца, напрыклад пад час перагляду прав або фільтрацыі небажанных элементаў.
const currentRole = "guest";
if (currentRole !== "admin") {
console.log("Access denied. Admins only.");
}
// "Access denied. Admins only."
Нестрогая аднаковасць (==) і яе ператварэння
Як прымусовая трансформацыя дае неспадзеваныя рэзультаты
Даўнае часам, прычыму як нормай стала строгая аднаковасць, большасць коду выкарыстоўвала ==. Калі типы операндаў разныя, слабая аднаковасць ператварае адны чы ўсія значэння на спакойны тип і тады яе парабягае. Рэзультаты такога парабягання часта не спадзяваюцца з інтуіцыяй.
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)
Знаменіты прыклад паказвае, сколькі схованых крокаў можа быць у такім процесе.
console.log([] == 0);
// true
Ёнкі дасягнуць true, двайчы ператварае порожній масэўкі на прымітны тип, што дае порожнюя строчка "". Тыпы заўсёды разныя, адтуды строчка парабягаецца з цалам, і яна ператвараецца на 0. Фінальны парабяг 0 == 0, які ў свою чаргу дае true.
Булевыя значэнні таксама ўскладненыя, таму што пры парабяганні з не-булевым значэннем яны ператвараюцца на цалыя раней, чым выконваецца ўсё іншае. true становіцца 1.
console.log(true == "true");
// false
Строка "true" не ператвараецца ў 1; калі яе ператварыць у число, яна стае NaN, а 1 не равна NaN. Самэй гэтага выраз value == true практычна ніколі не ўсунецца таго, чаго вы хочаце.
null і undefined паводзяцца за спецыяльным правілам: па прымерзлай роўнасці яны равны толькі аднам другаму, нават не 0, false чы пустай строце.
console.log(null == undefined);
// true
console.log(null == 0);
// false
console.log(null == false);
// false
console.log(null == "");
// false
console.log(undefined == 0);
// false
Перакананне на наявнасць null, калі == є дапустым
У савэчасным коде лепш утрымацца ад прымерзлай роўнасці, за ўзнакам адной шырока вжываемай асаблівасці. Пры обробцы дадзеных з API часта хочаце з’ясавіць, чы ў значэнні є null чы undefined, і выраз == null дае адказ на гэта за адну пораўнанне.
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"
Гэта функцыянаваець самэўсёлкава благодарна вышэйзгаданам правілу: null ў свободным значэнні равнае толькі null і undefined, таму значэнні, якія супадаюць з правілам «не-правды», але маюць сэнс, такія як 0 або "", праходзяць без змян. Гэта заменяе дліжэйшы варіант status === null || status === undefined. Зважайце, што багатыя команды насталяюць інструменты праблэм-детэкція так, каб абсалютна забарыць вжыванне ==, у такім случае трэба пісаць явную строгую версію. Напрыклад, правіло eqeqeq у ESLint мае опцыю, яка дазволяе самэўсёлкава вжыванне null у порэванні, але забарыць іншыя варыянты. У багатых случаях аператор з’еднання значэнняў, якія можаць быць null, ?? або опцыйнае ланкуванне ?., выражаюць намеры ўсё болей прымітна.
Свабодная неравасць (!=)
!= выконвае тыя жа перакладванні, як і ==, а пасля запярошвае рэзультат, таму яна спадчынава ўсе «парадоксы».
console.log(1 != "1");
// false (it converts "1" to a number, decides they are the same, so they are not different)
Тут "1" перакладваецца ў число 1, значэння аднародзяцца як роўныя, і таму перакладчык «нероўнасці» вяртае false. Ставіцеся да != з тым жа падазром, як і да ==, і калі ўсё неясна, выбірайце !==.
Спэльнія порэванні: <, >, <= і >=
Чысла і сумешаныя типы
Калі выкарыстоўваюцца два чысла, спэльнія аператары працуюць абсолютна так сама, як і ў арыфмэтычных вырахунках.
console.log(5 > 3);
// true
console.log(10 <= 10);
// true
Калі строка зустрэчаецца з чыслам, спачатку строка перакладваецца ў число. Строка, якая не прадставляе чысла, стае NaN, і будзь-якае спэльнае порэванні, якое включае NaN, дае false.
console.log("10" > 5);
// true
console.log("abc" > 3);
// false ("abc" becomes NaN, and NaN comparisons always return false)
Дзе разлічаюцца null і undefined
Аператары асоціяцыі паказваюць неадэкватнасць з аператарамі слабай роўнасці. == адмовляецца пераканваты null у число, але <, >, <= і >= пераканвалі яго ў 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)
Таким чынам null не роўнае ні 0, ні большае за 0, пры тым яно є большае або роўнае 0. Пасляльгівы рэзултат стае зрозумелым, калі знаць, што a >= b ацэнюецца як „a < b не ўсё правда“, а 0 < 0 — як неправда.
undefined зноў паводзіцца інакш. Пераканваны ў число яно стае NaN, таму кожная асоціяцыйная параболка, якая включае undefined, дае рэзултат false.
console.log(undefined > 0);
// false
console.log(undefined < 0);
// false
Строкі парабяруюцца за кодовыми елементамі, а не па порядку ў словніку
Калі оба операнды ў формате строк, не выконваецца их числовая канвертация. Строкі парабяруюцца символ за символам з левага, выкарыстоўваючы числовы код UTF-16 кожнага символа.
console.log("apple" < "banana");
// true (a comes before b)
console.log("b" > "a");
// true (b comes after a)
Верхнія літеры маюць меншыя кодовыя елементы, чым нижнія літеры, і гэта поясняе, чаму простая сортаванне можа паставіць "Zebra" раней, чым "apple".
console.log("Z" < "a");
// true (Z=90, a=97 in Unicode)
Для сортавання, якое адпавядае потрэбам людзей, выкарыстоўваюцца localeCompare() або Intl.Collator, якія дзеяць па правілах мовы, а не па чыстых кодовых елементах.
Адна з найбольш апасных проблем — строкі, якія маюць цифры. Парабяруючы іх як строкі, "10" ўважаецца меньшым за "9".
console.log("10" < "9");
// true (the character "1" is smaller than "9")
У JavaScript няма жаданняя, што гэтыя строкі выражаюць колькасці. Ягое апрацоўванне пачынаецца з першага символа кожной з іх — "1" проты "9"; пасля таго, калі выявляецца, што "1" стоіць першы, працэс завершваецца з рэзультатам true. Той жа эфект спостарэчыцца, калі масавек цых числаў сортуецца за дапамою sort() без заданага функцыі-кампаратара, адтако ў стандартным режыме сортавання элементы спачатку ператвараюцца ў строкі пры порэўнанні; каб сортуваць за числовым значэнням, трэба выкорыстаць (a, b) => a - b.
Рашэння — явна ператварыць числовыя строкі ў чысла пры перадзеўранні іх за велічынай.
console.log(Number("10") < Number("9"));
// false (correct: 10 is not less than 9)
Object.is() для абсалютнай ідэнтычнасці
Закрыцья «празораў» у ===
Object.is() — гэта найточнейшая перацэнка равенства, яку прысвячае JavaScript. Для звычных значэнняў яна дае тыя ж рэзультаты, чым строгая перацэнка равенства.
console.log(Object.is(1, 1));
// true
console.log(Object.is(1, "1"));
// false
Разлік між нимі полягае самэль у двух випадках стандарта IEEE 754. Функцыя Object.is() спрэтварае NaN до самога сябе:
console.log(Object.is(NaN, NaN));
// true
і яна розрізняе паазитыўны нуль ад аб’ектыўнага нуля:
console.log(Object.is(0, -0));
// false
Тры пераказы параду
1і"1":==вяртаеtrue;===іObject.is()вяртаеfalse.0іfalse:==вяртаеtrue;===іObject.is()вяртаеfalse.nullіundefined:==вяртаеtrue;===іObject.is()вяртаеfalse.
NaN і NaN: аператы == і === вяртаюць false; аперат Object.is() вяртае true.+0 і -0: аператы == і === вяртаюць true; аперат Object.is() вяртае false.[] і [], або {} і {}: усе тры аператы вяртаюць false, таму што это разныя об’екты.5 і 5: усе тры аператы вяртаюць true.Вы рэдка калі будзеце выклікаць Object.is() безпосередна, але гэта мае значэнне для інструментаў, якія вы вжываеце ўсё дзень. React апыягаецца Object.is(), каб выявіць, чы растоечна інфармацыя або залежнасць зменіліся і чы патрэбна перераскладка, і самэ гэта прычына, чаму змяненне об’екта на месцы не спрычынае апдэйта: рэферэнс застаецца ідэнтычным. Бібліятекі для керування растоечной інфармацыяй прыменяюць тую ж ідею; для глэбшага розумення пазірце як React-Redux выкарыстоўвае перакананні на равнасць, каб вырашыць пра перераскладку.
Ключовыя выводы
У першым сцэнарыі, калі адбываецца перакананне, якое павінна была адмовіць у доступе, не трэба нічага выключнага, кроме == для паўранення строкі з цячынай: тыхое перакананне прыводзіць да таго, што два разныя значэння выглядаюць роўнымі. Правіла, якія гэтага запобегаюць, простыя:
- Па ўсіх месцах, як правілу, выкарыстоўвайце
===і!==. - Утримайцеся ад
==і!=, за выключэнням намераванага скарыкатуvalue == null, якщо тое дазволяюць правілы лінтингу. - Для выявлення
NaNвыкарыстоўвайцеNumber.isNaN(), а не оператары роўнасці. - Памятайце, што порэванні об’ектаў пераказваюць ідэнтычнасць, а не ўместа.
- Перад выкарыстоўваннем
<або>пераканвертуйце строкі з числамі за дапамогоюNumber(), а таксама задайцеsort()компаратар для числаў. - Выкарыстоўвайце
Object.is(), калі трэба точна адмаўляцца сабе зNaNабо знакавым нулем.
Працясныя форматы пераканвертавання ў JavaScript не знікнуць, але выкарыстоўванне строгіх порэванняў не дазволяе ім паўтарыцца ў функціонале продакшна.