Сравнения и их ловушки
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Сравнение — это выражение, которое возвращает true или false. Всё честно и предсказуемо, пока с обеих сторон числа; дальше начинаются места, где JavaScript ведёт себя строго по правилам — и совершенно неожиданно.
Операторы >, <, >=, <= работают ровно так, как учили в школе. Непривычно другое: их результат — не «просто ответ на вопрос», а полноценное значение типа boolean. Его можно сохранить, передать в функцию, вернуть наружу.
Проверок на равенство в языке две: == и === (и зеркальные им != и !==). Разница между ними — главный источник ловушек, поэтому с неё и начнём.
=== сравнивает строго: сначала смотрит типы, потом значения. Типы разные — сразу false, без обсуждений. == ведёт себя мягче: он сперва пытается привести операнды к общему типу и только потом сравнивает — поэтому у него гораздо больше шансов ответить «равны».
Мягкость удобна ровно до того момента, когда становится вредной. То, что пустая строка равна нулю, а false равен нулю, не помогает ни в одной реальной задаче — зато прячет ошибки. Отсюда договорённость, принятая почти везде: по умолчанию пишем ===, а == достаём осознанно и редко.
Правило на каждый день: всегда ===. Живое исключение одно — проверка x == null, она разом ловит и null, и undefined.
Строки сравниваются не по длине и не по смыслу. Берутся первые символы: если они разные — ответ готов, если одинаковые — сравниваются вторые, и так далее. Каждый символ при этом сравнивается по своему номеру в Unicode.
Первая строка — ловушка, которую мы уже встречали в статье «Операторы и математика»; теперь разберём её механику до конца. "2" > "12" истинно, потому что сравниваются символы 2 и 1, а до второго символа дело просто не доходит. Если в переменных лежат числа, случайно оказавшиеся строками (пришли из формы, из URL, из JSON) — превращайте их в числа явно.
Потому что порядок Unicode и порядок алфавита — разные вещи. Все заглавные буквы идут раньше строчных, а ё в таблице стоит после я, хотя в словаре живёт между е и ж. Для человекочитаемой сортировки есть localeCompare: он знает правила языка.
Если операнды разных типов, операторы >, <, >=, <= и == приводят их к числам. Строка превращается в число, true — в 1, false — в 0. Исключение ровно одно: если обе стороны строки, работает посимвольное сравнение из прошлого раздела.
Последняя пара выглядит как поломка: строка "abc" не больше единицы и не меньше её. На самом деле всё логично — Number("abc") даёт NaN, а любое сравнение с NaN ложно. К этому вернёмся в конце.
| Значение | Во что превращается при сравнении |
|---|---|
| true | 1 |
| false | 0 |
| "" (пустая строка) | 0 |
| " 12 " | 12 |
| "abc" | NaN |
| null | 0 |
| undefined | NaN |
При нестрогом сравнении null и undefined равны друг другу — и никому больше. Это отдельное правило языка, а не приведение типов: ни с нулём, ни с пустой строкой, ни с false они не совпадают. А ===, как обычно, честно видит разные типы.
А теперь странность, которая ломает интуицию: null > 0 — ложь, null == 0 — тоже ложь, но null >= 0 — правда. Противоречия нет: >= и == живут по разным правилам. Сравнение по величине честно превращает null в 0, а == для null идёт своим особым путём и с числами его вообще не сравнивает.
Практический вывод простой: не пускайте null и undefined в сравнения по величине. Сначала убедитесь, что значение вообще есть, и только потом сравнивайте — тогда таких загадок в коде не заведётся.
NaN — «не число» — появляется там, где вычисление не удалось: Number("abc"), 0 / 0, Math.sqrt(-1). Особенность у него одна, но крупная: NaN не равен ничему, включая самого себя.
Поэтому проверять на NaN сравнением бессмысленно — для этого есть Number.isNaN(). А если хочется сравнить «как есть», без сюрпризов, пригодится Object.is(): он работает как ===, но считает NaN равным NaN и различает 0 и -0. В повседневном коде он нужен редко, но знать о нём полезно.
Старая глобальная isNaN() сначала приводит аргумент к числу и только потом проверяет — поэтому она говорит «да» про любую нечисловую строку. Number.isNaN() ничего не приводит и отвечает честно: это именно NaN или нет. Берите вторую.
- Сравнение возвращает обычное значение типа boolean — его можно сохранить и передать дальше.
- === сравнивает типы и значения; == сначала приводит типы, поэтому по умолчанию пишем ===.
- Две строки сравниваются посимвольно по кодам Unicode: "2" > "12" — правда.
- При разных типах операнды становятся числами; null и undefined равны только друг другу, но null >= 0 при этом истинно.
- NaN не равен ничему, включая себя: проверяйте через Number.isNaN(), а Object.is() нужен для сравнения «как есть».
1/3Что выведет console.log("2" > "12") ?
2/3Почему null >= 0 даёт true, а null == 0 даёт false?
3/3Как надёжно проверить, что в переменной x лежит NaN?
Починить проверку веса
Функция отвечает, влезает ли посылка в лимит. Значения приходят из формы, то есть строками. Объясните, почему проверка врёт, и почините её так, чтобы она работала и со строками, и с числами.
Обе стороны — строки, поэтому сравнение идёт посимвольно: "9" против "1". Символ 9 в Unicode стоит позже, значит "9" <= "10" — ложь, и длина строк роли не играет.
Лечится явным приведением к числу:
Общее правило: приводите данные к нужному типу сразу на входе, а не надейтесь, что сравнение догадается само.
Проверка на «значения нет»
Напишите функцию isMissing(value): она возвращает true, если значение — null или undefined, и false для всего остального, включая 0 и пустую строку. Тело функции должно уместиться в одну строку.
Это тот самый случай, ради которого == оставляют в языке: при нестрогом сравнении null равен только undefined и самому себе, поэтому одна проверка ловит оба значения и не задевает 0 и пустую строку.
Через === пришлось бы писать длиннее — и это тоже правильный ответ:
А вот if (!value) здесь не подходит: 0 и "" тоже ложны, и функция начнёт врать.