Однажды я собирал материалы чтобы устроить ликбез по JavaScript для пары коллег. Тогда я и наткнулся на довольно интересный пример, в котором рассматривалось сравнение значения null
с нулём. Собственно говоря, вот этот пример:
null > 0; // false
null == 0; // false
null >= 0; // true
На первый взгляд — полная бессмыслица. Как некое значение может быть не больше, чем 0, не равно нулю, но при этом быть больше или равно нулю?
Хотя поначалу я оставил это без особого внимания, решив, что всё дело в том, что JavaScript — это JavaScript, со всеми его странностями, этот пример меня заинтриговал. Связано ли это с типом null
и с тем, как он обрабатывается, или с тем, как выполняются операции сравнения значений?
В итоге я решил докопаться до сути происходящего и начал рыться в единственном источнике истины для JavaScript — в спецификации ECMA. Сегодня я хочу рассказать вам о том, в какую глубокую кроличью нору я в результате провалился.
Абстрактный алгоритм сравнения для отношений
Рассмотрим первое сравнение:
null > 0; // false
В соответствии со спецификацией, операторы сравнения >
и <
, для того, чтобы выяснить, истинно или ложно выражение, пропускают его через так называемый абстрактный алгоритм сравнения для отношений. Здесь и далее мы будем цитировать фрагменты спецификации по тексту перевода «Стандарт ECMA-262, 3я редакция» с ресурса javascript.ru:
Сравнение x < y, где x и y являются значениями, возвращает true, false или undefined (последнее означает, что хотя бы один из операндов равен NaN). Такое сравнение производится следующим образом:
1. Вызвать ToPrimitive(x, подсказка Number).
2. Вызвать ToPrimitive(y, подсказка Number).
3. Если Тип(Результата(1)) равен String и Тип(Результата(2)) равен String - переход на шаг 16. (Заметим, что этот шаг отличается от шага 7 в алгоритме для оператора сложения + тем, что в нём используется и вместо или.)
4. Вызвать ToNumber(Результат(1)).
5. Вызвать ToNumber(Результат(2)).
6. Если Результат(4) равен NaN - вернуть undefined.
7. Если Результат(5) равен NaN - вернуть undefined.
8. Если Результат(4) и Результат(5) являются одинаковыми числовыми значениями - вернуть false.
9. Если Результат(4) равен +0 и Результат(5) равен -0 - вернуть false.
10. Если Результат(4) равен -0 и Результат(5) равен +0 - вернуть false.
11. Если Результат(4) равен +∞, вернуть false.
12. Если Результат(5) равен +∞, вернуть true.
13. Если Результат(5) равен -∞, вернуть false.
14. Если Результат(4) равен -∞, вернуть true.
15. Если математическое значение Результата (4) меньше, чем математическое значение Результата(5) (заметим, что эти математические значения оба конечны и не равны нулю) - вернуть true. Иначе вернуть false.
16. Если Результат(2) является префиксом Результата(1), вернуть false. (Строковое значение p является префиксом строкового значения q, если q может быть результатом конкатенации p и некоторой другой строки r. Отметим, что каждая строка является своим префиксом, т.к. r может быть пустой строкой.)
17. Если Результат(1) является префиксом Результата(2), вернуть true.
18. Пусть k - наименьшее неотрицательное число такое, что символ на позиции k Результата(1) отличается от символа на позиции k Результата(2). (Такое k должно существовать, т.к. на данном шаге установлено, что ни одна из строк не является префиксом другой.)
19. Пусть m - целое, равное юникодному коду символа на позиции k строки Результат(1).
20. Пусть n - целое, равное юникодному коду символа на позиции k строки Результат(2).
21. Если m < n, вернуть true. Иначе вернуть false.
Пройдёмся по этому алгоритму с нашим выражением null > 0
.
Шаги 1 и 2 предлагают нам вызвать оператор ToPrimitive()
для значений null
и 0
для того, чтобы привести эти значения к их элементарному типу (к такому, например, как Number
или String
). Вот как ToPrimitive преобразует различные значения:
Входной тип | Результат |
Undefined | Преобразование не производится |
Null | Преобразование не производится |
Boolean | Преобразование не производится |
Number | Преобразование не производится |
String | Преобразование не производится |
Object | Возвращает значение по умолчанию для объекта. Значение по умолчанию для объекта получается путём вызова для объекта внутреннего метода [[DefaultValue]] с передачей ему опциональной подсказки ПредпочтительныйТип. |
В соответствии с таблицей, ни к левой части выражения, null
, ни к правой части, 0
, никаких преобразований не применяется.
Шаг 3 алгоритма в нашем случае неприменим, пропускаем его и идём дальше. На шагах 4 и 5 нам нужно преобразовать левую и правую части выражения к типу Number
. Преобразование к типу Number выполняется в соответствии со следующей таблицей (здесь опущены правила преобразования для входных типов String
и Object
, так как они к теме нашего разговора отношения не имеют):
Входной тип | Результат |
Undefined | NaN |
Null | +0 |
Boolean | Результат равен 1, если аргумент равен true. Результат равен +0, если аргумент равен false. |
Number | Преобразование не производится |
… | … |
В соответствии с таблицей, null
будет преобразовано в +0
, а 0
останется самим собой. Ни одно из этих значений не является NaN
, поэтому шаги алгоритма 6 и 7 можно пропустить. А вот на шаге 8 нам надо остановиться. Значение +0
равно 0
, в результате алгоритм возвращает false
. Таким образом:
null > 0; // false
null < 0; // тоже false
Итак, почему null
не больше и не меньше нуля мы выяснили. Теперь идём дальше — разберёмся с тем, почему null
ещё и не равен нулю.
Абстрактный алгоритм сравнения для равенств
Рассмотрим теперь проверку на равенство null
и 0
:
null == 0; //false
Оператор ==
использует так называемый абстрактный алгоритм сравнения для равенств, возвращая в результате true
или false
. Вот этот алгоритм:
Сравнение x == y, где x и y являются значениями, возвращает true или false. Такое сравнение производится следующим образом:
1. Если Тип(x) отличается от Типа(y) - переход на шаг 14.
2. Если Тип(x) равен Undefined - вернуть true.
3. Если Тип(x) равен Null - вернуть true.
4. Если Тип(x) не равен Number - переход на шаг 11.
5. Если x является NaN - вернуть false.
6. Если y является NaN - вернуть false.
7. Если x является таким же числовым значением, что и y, - вернуть true.
8. Если x равен +0, а y равен -0, вернуть true.
9. Если x равен -0, а y равен +0, вернуть true.
10. Вернуть false.
11. Если Тип(x) равен String - вернуть true, если x и y являются в точности одинаковыми последовательностями символов (имеют одинаковую длину и одинаковые символы в соответствующих позициях). Иначе вернуть false.
12. Если Тип(x) равен Boolean, вернуть true, если x и y оба равны true или оба равны false. Иначе вернуть false.
13. Вернуть true, если x и y ссылаются на один и тот же объект или они ссылаются на объекты, которые были объединены вместе (см. раздел 13.1.2). Иначе вернуть false.
14. Если x равно null, а y равно undefined - вернуть true.
15. Если x равно undefined, а y равно null - вернуть true.
16. Если Тип(x) равен Number, а Тип(y) равен String, вернуть результат сравнения x == ToNumber(y).
17. Если Тип(x) равен String, а Тип(y) равен Number, вернуть результат сравнения ToNumber(x)== y.
18. Если Тип(x) равен Boolean, вернуть результат сравнения ToNumber(x)== y.
19. Если Тип(y) равен Boolean, вернуть результат сравнения x == ToNumber(y).
20. Если Тип(x) - String или Number, а Тип(y) - Object, вернуть результат сравнения x == ToPrimitive(y).
21. Если Тип(x) - Object, а Тип(y) - String или Number, вернуть результат сравнения ToPrimitive(x)== y.
22. Вернуть false.
Пытаясь понять, равно ли значение null
значению 0, мы сразу переходим из шага 1 к шагу 14, так как Тип(x) отличается от Типа(y)
. Как ни странно, но шаги 14-21 тоже к нашему случаю не подходят, так как Тип(х) —
это null
. Наконец мы попадаем на шаг 22, после чего false
возвращается как значение по умолчанию!
В результате и оказывается, что:
null == 0; //false
Теперь, когда ещё одна «тайна» JavaScript» раскрыта, разберёмся с оператором «больше или равно».
Оператор больше или равно (>=)
Выясним теперь, почему истинно такое выражение:
null >= 0; // true
Тут спецификация полностью выбила меня из колеи. Вот как, на очень высоком уровне, работает оператор >=:
Если null < 0 принимает значение false, то null >= 0 принимает значение true
В результате мы и получаем:
null >= 0; // true
И, на самом деле, в этом есть смысл. С точки зрения математики, если у нас есть два числа, x
и y
, и если x
не меньше, чем y
, тогда x
должно быть больше чем y
или равно ему.
Я предполагаю, что данный оператор работает именно так для того, чтобы оптимизировать вычисления. Зачем сначала проверять, больше ли x
чем y
, и, если это не так, проверять, равняется ли значение x
значению y
, если можно выполнить всего одно сравнение, проверить, меньше ли x
чем y
, а затем использовать результат этого сравнения для того, чтобы вывести результат исходного выражения.
Итоги
Вопрос о сравнении null
и 0
, на самом деле, не такой уж и сложный. Однако, поиск ответа открыл мне кое-что новое о JavaScript. Надеюсь, мой рассказ сделал то же самое для вас.
Уважаемые читатели! Знаете ли вы о каких-нибудь странностях JavaScript, которые, после чтения документации, уже перестают казаться таковыми?
Автор: ru_vds