Галоўная / Артыкулы

Паспрацаванне з абэраваннем у JavaScript: ===, ==, аперацыі стосункоў і Object.is

Дазвольце дазнацься, як строгая і вольная равнасць, аблікацыйныя порэўнанні і Object.is насправдзе выбіраюць аднаковасць у JavaScript, а таксама пра пасткі прымусовага перакантравання, якія хавае кожны з іх.

Выдавец: REACTAPP.TOP10 хв чытання2405 слоў

Крыніцы: 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 выкарыстоўвае перакананні на равнасць, каб вырашыць пра перераскладку.

    Ключовыя выводы

    У першым сцэнарыі, калі адбываецца перакананне, якое павінна была адмовіць у доступе, не трэба нічага выключнага, кроме == для паўранення строкі з цячынай: тыхое перакананне прыводзіць да таго, што два разныя значэння выглядаюць роўнымі. Правіла, якія гэтага запобегаюць, простыя:

    1. Па ўсіх месцах, як правілу, выкарыстоўвайце === і !==.
    2. Утримайцеся ад == і !=, за выключэнням намераванага скарыкату value == null, якщо тое дазволяюць правілы лінтингу.
    3. Для выявлення NaN выкарыстоўвайце Number.isNaN(), а не оператары роўнасці.
    4. Памятайце, што порэванні об’ектаў пераказваюць ідэнтычнасць, а не ўместа.
    5. Перад выкарыстоўваннем < або > пераканвертуйце строкі з числамі за дапамогою Number(), а таксама задайце sort() компаратар для числаў.
    6. Выкарыстоўвайце Object.is(), калі трэба точна адмаўляцца сабе з NaN або знакавым нулем.

    Працясныя форматы пераканвертавання ў JavaScript не знікнуць, але выкарыстоўванне строгіх порэванняў не дазволяе ім паўтарыцца ў функціонале продакшна.