Тема сравнения значений и тесно переплетенная с ней тема приведения типов относятся к самой фундаментальной области знаний для программистов на Javascript, без этих знаний будет легко допустить ошибки, и в каких-то моментах поведение программы может показаться нелогичным. Эта тема — такой же фундамент, как и тема функций и ООП в JS, о которых я уже писал цикл статей (кстати, надо бы сесть и обновить их, привести к текущим реалиям языка). Итак, постараюсь изложить максимально коротко, насколько это позволяют важность темы и подводные камни языка.
Как известно, для управления ходом программы (в операторах if, for, while…) или для выбора значения (тернарный оператор … ? … : … ) используются логические значения — true и false. Получить их можно двумя способами (мы не будем рассматривать логические операторы, такие как &&, ||, они вторичны).
Первый способ: получить логическое значение путем преобразования типов
Например:
1 2 3 |
console.log(Boolean(0)); // false console.log(!0); // true console.log(!!0); // false - аналог Boolean(0) |
Правила преобразования довольно простые:
false получается из:
- Числа 0, специального значения NaN (Not a Number — число, которое не число, а ошибка)
- null
- undefined
- Пустой строки (совсем пустой, 0 символов)
true получается из всего остального:
- Любого числа, кроме 0 и NaN
- Непустой строки, в том числе, состоящей из пробелов или «0»
- Любого объекта, в том числе «пустого», пустого массива, функции, и даже если нечаянно создать объект класса Boolean:
1 2 3 4 |
console.log(!!{}); // true console.log(!![]); // true console.log(Boolean(0)); // false - все в порядке console.log(!!new Boolean(0)); // true - осторожно, new создает объект! |
Кстати, как узнать, что в переменной объект? Нужно определить, к какому из 6 типов она относится — это три примитивных (number, string, boolean), два специальных пустых типа (undefined, null) и, собственно, объектный тип (object). Для этого в JS существует оператор typeof, но у него есть некоторые тонкие моменты:
1 2 3 |
console.log(typeof []); // "object" - массивы - это объекты, типа "массив" в JS нет console.log(typeof null); // "object" - это официальный глюк JS, смиритесь console.log(typeof function(){}); // "function" - хотя функция - объект |
Определить, что в переменной массив, можно двумя способами:
1 2 |
console.log([] instanceof Array); // true console.log(Array.isArray([])); // true |
Второй способ: операторы сравнения
Строгое равенство (===)
Самый простой и понятный вариант: два операнда сравниваются сначала по типам, если тип один, идет сравнение по значению. Для примитивных типов есть одно исключение — NaN при любых сравнениях, даже с собой, всегда дает false. А объекты равны только в случае, если оба операнда ссылаются на один и тот же объект:
1 2 3 4 5 6 |
console.log(0 === 0); // true console.log(NaN === NaN); // false - особый случай console.log(0 === "0"); // false - разные типы console.log([] === []); // false - разные объекты, хоть и похожи var a = [], b = a; console.log(a === b); // true - один и тот же объект |
Нестрогое или абстрактное равенство (==)
Запомните, что это опасная штука со множеством нюансов:
- null == undefined дает true — это надо запомнить (null === undefined при этом дает false)
- Нестрогое проверка на равенство null и undefined с чем угодно другим всегда дает false
- NaN, как и при строгом равенстве, ни с чем сравнивать нельзя — всегда будет false
- Два объекта равны, только если это один и тот же объект
- Два примитива одного типа сравниваются по значению — тут без сюрпризов
- Два операнда разного типа приводятся к числовому типу и после этого сравниваются по значению (об этом будет ниже)
Сравнение на больше-меньше (>, <, >=, <=)
- Числа сравниваются без сюрпризов, не забываем про NaN, сравнение с которым всегда дает false
- Две строки сравниваются лексикографически, то есть «как в словаре», но учитывая расположение символов в кодовой таблице Unicode
- Во всех остальных случаях оба операнда приводятся к числовому типу и получившиеся значения сравниваются
Приведение к числовому типу в Javascript
- null и false превращаются в 0
- true превращается в 1
- undefined превращается в NaN со всеми вытекающими проблемами для сравнения — любое сравнение даст false!
- У строк сначала отбрасываются пробельные символы в начале и в конце, если ничего не осталось, то это 0, если остается какое-то число, в том числе с десятичной точкой («1.23»), с минусом в начале или заданное через экспоненту («1e3» — это 1*10³, то есть 1000), то оно считывается, иначе — это NaN, опять же, не сравнимая ни с чем величина. Учтите, что функции parseInt() и parseFloat() работают немного по-другому — они позволяют иметь в строке после числа любые другие символы, они просто проигнорируются. Кроме того, parseInt() умеет получать из строки не только десятичные числа, но и числа с другими основаниями — двоичные, шестнадцатеричные и т.д., но в случае строки «1e3» считывает только 1, остальное игнорирует
- У объектов вызывается метод valueOf(), а если его нет или он возвращает не примитив, то вызывается toString(). Далее, если полученный примитив не является числом, то идет следующий этап приведения к числу по правилам выше.
Некоторые дефолтные преобразования объектов в числа (если мы не задали соответствующие методы для объекта или его прототипа):
- Метод toString() у массивов дает строку, в которой через запятую перечислены элементы массива. Эту строку пробуем затем превратить в число, и тут уже все зависит от содержимого массива, см. примеры ниже
- Тот же метод для обычных объектов возвращает строку «[object Object]», поэтому результатом преобразования в число будет NaN
- Для функций возвращается строка с содержимым кода функции, начиная со слова function, поэтому преобразование в число снова дает NaN
- Для дат (объектов класса Date) метод valueOf() вернет количество миллисекунд с начала Эпохи Юникса. Поэтому есть интересное следствие: если есть два объекта даты, указывающие на одну и ту же временную точку, то строгое или нестрогое равенство (===, ==) вернет false, потому что это разные объекты, а вот если сравнить через >= или <=, либо же оба операнда принудительно привести к числу, то вернется true, потому что число миллисекунд одинаково.
Примеры преобразований к числу (этого можно добиться, воспользовавшись унарным плюсом или через Number()):
1 2 3 4 5 6 7 8 9 10 11 12 |
console.log(+true); // 1 console.log(+null); // 0 console.log(+undefined); // NaN console.log(+" 1e3 "); // 1000 console.log(+[]); // 0 - сначала получаем пустую строку, она превращается в 0 console.log(+[1]); // 1 - сначала получаем строку "1", она превращается в 1 console.log(+[1, 2, 3]); // NaN - сначала получаем строку "1,2,3", она превращается в NaN console.log(+{}); // NaN - сначала получается строка "[object Object]" console.log(+(new Date)); // 1561229499856 - у вас будет другое текущее время var obj = {}; console.log(+obj.someField); // NaN - обращение с несуществующему полю дает undefined |
Разные примеры сравнения, в том числе затейливые:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
console.log(NaN === NaN); // false - несравнимая ни с чем величина NaN console.log(undefined === null); // false console.log(undefined == null); // true - просто запомнить console.log(undefined == 0); // false console.log(null == 0); // false console.log(undefined >= 0); // false - undefined становится NaN console.log(null >= 0); // true - null становится 0 console.log(1 === "1"); // false console.log(1 == " 1 "); // true console.log(1 == [1]); // true console.log(0 == []); // true console.log([] == []); // false - разные объекты console.log(1 == true); // true console.log(true > []); // true - потому что true становится 1, а [] - 0 console.log("банан" > "арбуз"); // true console.log("Банан" > "арбуз"); // false - заглавные идут раньше строчных console.log("2" > "11"); // false - это строки текста, а не числа var date1 = new Date('2019-06-22T12:34:56'); var date2 = new Date('2019-06-22T12:34:56'); console.log(date1 == date2); // false - разные объекты console.log(date1 >= date2); // true - одинаковое число миллисекунд console.log([] == ![]); // true |
Последний пример кажется совсем бредом, но с точки зрения JS, все логично: сначала массив справа приводится к булевому типу, а любой объект — это true, с помощью оператора отрицания (!) значение превращается в false (собственно, из-за этого оператора массив и преобразуется в логическое значение). Далее false сравнивается нестрого с массивом — задействуется механизм приведения к числовому типу, и оба операнда превращаются в 0, получается 0 === 0.
А вот еще пример, когда оказывается, что истина истине рознь, и проявляются нюансы автоматических преобразований типов, заложенные в языке:
1 2 3 4 5 6 7 8 9 |
var str = '0'; if (str) { // преобразуется в true console.log('Непустая строка'); // это сработает } if (str == true) { // преобразуется в проверку равенства 0 === 1 console.log('Непустая строка'); // а это не сработает! } |
Важное замечание. Я не рассматривал операторы != и !==, потому они — по сути, оператор отрицания, примененный к результату операторов равенства == и === соответственно. То есть x != y — это то же самое, что !(x == y).
Бонус: оператор switch-case
Не забывайте, что оператор switch-case производит проверку на строгое равенство (===), поэтому могут возникнуть казусы, например, если вы берете значение для проверки из формы — оно вам вернется строкового типа, его нельзя сравнивать с числами в case:
1 2 3 4 5 6 7 8 9 10 |
var select = document.getElementById('answer'); // где-то в html есть select с id="answer" var answer = select.value; switch (answer) { case 1: alert("Верно!"); break; case 2: alert("Неверно!"); break; } |
В данном примере ничего не произойдет, потому что в переменной answer значение типа string, а в разделах case — значения типа number.
Мораль и итог
Старайтесь не полагаться на магию, всегда контролируйте типы переменных, производите ручное преобразование типов, используйте оператор строгого равенства, и помните, что javascript не так прост, как кажется на первый взгляд.