Сравнения и их ловушки

ветвления≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Сравнение — это выражение, которое возвращает true или false. Всё честно и предсказуемо, пока с обеих сторон числа; дальше начинаются места, где JavaScript ведёт себя строго по правилам — и совершенно неожиданно.

Разделы темы:·

Операторы >, <, >=, <= работают ровно так, как учили в школе. Непривычно другое: их результат — не «просто ответ на вопрос», а полноценное значение типа boolean. Его можно сохранить, передать в функцию, вернуть наружу.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Проверок на равенство в языке две: == и === (и зеркальные им != и !==). Разница между ними — главный источник ловушек, поэтому с неё и начнём.

=== сравнивает строго: сначала смотрит типы, потом значения. Типы разные — сразу false, без обсуждений. == ведёт себя мягче: он сперва пытается привести операнды к общему типу и только потом сравнивает — поэтому у него гораздо больше шансов ответить «равны».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Мягкость удобна ровно до того момента, когда становится вредной. То, что пустая строка равна нулю, а false равен нулю, не помогает ни в одной реальной задаче — зато прячет ошибки. Отсюда договорённость, принятая почти везде: по умолчанию пишем ===, а == достаём осознанно и редко.

Правило на каждый день: всегда ===. Живое исключение одно — проверка x == null, она разом ловит и null, и undefined.

Строки сравниваются не по длине и не по смыслу. Берутся первые символы: если они разные — ответ готов, если одинаковые — сравниваются вторые, и так далее. Каждый символ при этом сравнивается по своему номеру в Unicode.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Первая строка — ловушка, которую мы уже встречали в статье «Операторы и математика»; теперь разберём её механику до конца. "2" > "12" истинно, потому что сравниваются символы 2 и 1, а до второго символа дело просто не доходит. Если в переменных лежат числа, случайно оказавшиеся строками (пришли из формы, из URL, из JSON) — превращайте их в числа явно.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Потому что порядок Unicode и порядок алфавита — разные вещи. Все заглавные буквы идут раньше строчных, а ё в таблице стоит после я, хотя в словаре живёт между е и ж. Для человекочитаемой сортировки есть localeCompare: он знает правила языка.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Если операнды разных типов, операторы >, <, >=, <= и == приводят их к числам. Строка превращается в число, true — в 1, false — в 0. Исключение ровно одно: если обе стороны строки, работает посимвольное сравнение из прошлого раздела.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Последняя пара выглядит как поломка: строка "abc" не больше единицы и не меньше её. На самом деле всё логично — Number("abc") даёт NaN, а любое сравнение с NaN ложно. К этому вернёмся в конце.

ЗначениеВо что превращается при сравнении
true1
false0
"" (пустая строка)0
" 12 "12
"abc"NaN
null0
undefinedNaN

При нестрогом сравнении null и undefined равны друг другу — и никому больше. Это отдельное правило языка, а не приведение типов: ни с нулём, ни с пустой строкой, ни с false они не совпадают. А ===, как обычно, честно видит разные типы.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

А теперь странность, которая ломает интуицию: null > 0 — ложь, null == 0 — тоже ложь, но null >= 0 — правда. Противоречия нет: >= и == живут по разным правилам. Сравнение по величине честно превращает null в 0, а == для null идёт своим особым путём и с числами его вообще не сравнивает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Практический вывод простой: не пускайте null и undefined в сравнения по величине. Сначала убедитесь, что значение вообще есть, и только потом сравнивайте — тогда таких загадок в коде не заведётся.

NaN — «не число» — появляется там, где вычисление не удалось: Number("abc"), 0 / 0, Math.sqrt(-1). Особенность у него одна, но крупная: NaN не равен ничему, включая самого себя.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Поэтому проверять на NaN сравнением бессмысленно — для этого есть Number.isNaN(). А если хочется сравнить «как есть», без сюрпризов, пригодится Object.is(): он работает как ===, но считает NaN равным NaN и различает 0 и -0. В повседневном коде он нужен редко, но знать о нём полезно.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Старая глобальная isNaN() сначала приводит аргумент к числу и только потом проверяет — поэтому она говорит «да» про любую нечисловую строку. Number.isNaN() ничего не приводит и отвечает честно: это именно NaN или нет. Берите вторую.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

  • Сравнение возвращает обычное значение типа 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?

Починить проверку веса

Функция отвечает, влезает ли посылка в лимит. Значения приходят из формы, то есть строками. Объясните, почему проверка врёт, и почините её так, чтобы она работала и со строками, и с числами.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Обе стороны — строки, поэтому сравнение идёт посимвольно: "9" против "1". Символ 9 в Unicode стоит позже, значит "9" <= "10" — ложь, и длина строк роли не играет.

Лечится явным приведением к числу:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Общее правило: приводите данные к нужному типу сразу на входе, а не надейтесь, что сравнение догадается само.

Проверка на «значения нет»

Напишите функцию isMissing(value): она возвращает true, если значение — null или undefined, и false для всего остального, включая 0 и пустую строку. Тело функции должно уместиться в одну строку.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Это тот самый случай, ради которого == оставляют в языке: при нестрогом сравнении null равен только undefined и самому себе, поэтому одна проверка ловит оба значения и не задевает 0 и пустую строку.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Через === пришлось бы писать длиннее — и это тоже правильный ответ:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

А вот if (!value) здесь не подходит: 0 и "" тоже ложны, и функция начнёт врать.