Однажды я собирал материалы чтобы устроить ликбез по 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, которые, после чтения документации, уже перестают казаться таковыми?
Комментарии (49)
kalininmr
12.09.2017 15:42+4казалось бы.
> Если null < 0 принимает значение false, то null >= 0 принимает значение true
тут все логично
получается всю математическую логику ломает странный ==khim
12.09.2017 18:27+3Там (так же, как и в PHP) всё странное. Для многих алгиритмов нужно отношение порядка или отношение эквивалентности. Ни того, ни другого ни PHP, ни в JavaScript по умолчанию нету — что совершенно логично (эксивалентность достигается с помощью ===, а порядок… нету порядка).
Дело в том, что для того, что операторы сравнения работали разумно в нетипизированных языках нужно не два результата, а три:true
,false
, unorderable types.
Но поскольку PHP и JavaScript писались не для программистов, то было принято решение — программа должна работать любой ценой! Великолепное Job Security, так как без навешивания костылей (типа компилятора с TypeScript или ClojureScript) ошибки можно искать вечно…kalininmr
12.09.2017 21:09впринципе то понятно, null == 0 — False вполне логично… хотя в С будет True :)
Aberro
12.09.2017 15:54+4«И, на самом деле, в этом есть смысл. С точки зрения математики, если у нас есть два числа, x и y, и если x не меньше, чем y, тогда x должно быть больше чем y или равно ему.»
Если нельзя вернуть неопределённый результат, а нужно вернуть только true или false, то как насчёт векторов, матриц? Если один вектор не меньше другого, это ведь не значит, что он больше или равен, это значит, что он не меньше, не больше, не равен и вообще не подлежит ранжированию. Так что с точки зрения математики определять один оператор через результат другого — глупо.MacIn
12.09.2017 16:42Просто в вашем случае область определения функции сравнения иная — есть еще дискретный «некорректное значение». А здесь область определения чисто булева, так что все корректно.
0xd34df00d
13.09.2017 21:46Эм, нет. В том случае задан частичный порядок (хотя можно задать и полный, но он, по крайней мере, для матриц не будет согласован с умножением, что не очень приятно), а на вещественных числах таки есть полный порядок, согласованный с операциями на этом кольце.
А в математике отношение «меньше» на множестве A — это именно что бинарное отношение, то есть подмножество множества (A x A). Соответственно «x больше чем y» следует из «y меньше чем x», а не из «x не меньше чем y» по описанной в исходном комментарии причине.
kahi4
12.09.2017 16:05+2RelationalExpression : RelationalExpression >= ShiftExpression 1. Let lref be the result of evaluating RelationalExpression. 2. Let lval be ? GetValue(lref). 3. Let rref be the result of evaluating ShiftExpression. 4. Let rval be ? GetValue(rref). 5. Let r be the result of performing Abstract Relational Comparison lval < rval. 6. ReturnIfAbrupt (r). 7. If r is true or undefined, return false. Otherwise, return true
Источник раздел 12.10 (осторожно, большая PDF)
Справедливости ради, эта строчка немного не точная:
Если null < 0 принимает значение false, то null >= 0 принимает значение true
Фактически, спецификация говорит о том, что нужно вернуть обратное значение операнду
lval < rval
и справедлива для любого набора данных (кроме, разумеется undefined за счет ReturnIfAbrupt). В статье так и написано, но в переводе спецификации как-то странно, может в старой версии так и было.
anfilat
12.09.2017 16:29+2Авторы стандарта так и не решили для себя окончательно, что такое null — особый тип объекта или особый тип нуля, вот и получился баг в спецификации
pm_wanderer
12.09.2017 18:38-4Смотрящим за стандартом ES давно пора пометить undefined и null как obsolete и заменить на условный nil — «нет значения».
А по-хорошему вообще ввести новый режим браузера, типа «use superstrict»: оставить там все лучшее и выкинуть устаревшее.kuraga333
13.09.2017 07:35+5Пожалуй, за undefined как раз и стоит похвалить JavaScript…
pm_wanderer
13.09.2017 11:47+1У него семантический смысл — переменной еще небыло присвоено значение.
Следовательно, не совсем корректно undefined присваивать самостоятельно.
Я считаю, что надо ввести новое примитивное значение nil, семантический смысл которого — «просто нет значения». Nil будет унифицировать синтаксис и сочетать в себе лучшее от undefined и null. Сейчас по сути это и так происходит в TypeScript — при компиляции там не используется null и все решается через undefined. Я лишь предлагаю другое имя)mayorovp
13.09.2017 12:46Сейчас по сути это и так происходит в TypeScript — при компиляции там не используется null и все решается через undefined.
А можно пруф?
pm_wanderer
13.09.2017 16:11+1Немного напутал — null не применяется в разработке самого языка:
null and undefined
kuraga333
13.09.2017 19:11Для меня undefined — «значения нет», null — «пустое значение». Условно говоря, переменная — ссылка, undefined — отсутствие значения у этой ссылки, а null — отсутствие значения по ссылке. То есть, если мы не можем найти значение чего-либо — имеем undefined, если можем, но оно пустое — то null.
В моем представлении (хотя я не теоретик программирования), в идеале, undefined — отдельный тип, а null и Na{N,T,etc} — значения, присутствующие в каждом типе. То есть undefined — это «NULL», а null — "\Lambda". И NaN (некорректное значение) нужен.
Представьте не переменные, а ассоциативный массив. Хотя тут в JavaScript'е не хватает «строгого» взятия элемента ассоциативного массива, с исключением в случе отсутствия в нем данного ключа.
Эх, наверное, заминусуют…
Но, порывшись, я понял, что спесификация со мной не согласна:
The undefined value is a primitive value used when a variable has not been assigned a value.
The null value is a primitive value that represents the null, empty, or non-existent reference.
pm_wanderer
14.09.2017 01:12+1Именно такое значение по спецификации я имею ввиду — она очень уж узко определяет эти вещи. Думаю в будущем это будет исправлено и унифицировано. Самый простой вариант на данный момент — просто отказаться от null и принять undefined за «просто нет значения», как Вы и считали изначально)
vanxant
15.09.2017 22:44Вообще, конечно, null должен быть «пустым значением» (математики бы назвали единицей относительно операции сложения для данного типа). Числом 0, пустой строкой, массивом из 0 элементов, пустым объектом без свойств. Но при этом — штатным значением, неразрушающим другие значения при участии в операциях, и, главное, типизированным. И не для каждого типа вообще существовать (примеры типов без нуля: boolean, сложные классы типа user и т.д.)
А undefined — именно что нештатным, нетипизированным и разрушающим, для ошибочных ситуаций.
К сожалению, при создании первых версий js до этого не додумались, а потом уже было поздно.
Varim
12.09.2017 20:45-3И, на самом деле, в этом есть смысл. С точки зрения математики, если у нас есть два числа, x и y, и если x не меньше, чем y, тогда x должно быть больше чем y или равно ему.
Нелогично. Тренарная логика и базы данных несогласны.
true, false, null, undefined это 3х или даже 4х арная логика.
CyberSoft
12.09.2017 21:52+8С точки зрения математики, если у нас есть два числа, x и y, и если x не меньше, чем y, тогда x должно быть больше чем y или равно ему.
Тут-то всё логично, но раз уж схватились за математику, то этот код:
null > 0; // false null == 0; // false null >= 0; // true
с её точки зрения нарушает очевидное математическое утверждение:
A >= B это тоже самое, что A > B || A == B
vlivyur
13.09.2017 11:00+1Ну с математикой там всё сложно, особенно если учесть что null <= 0 тоже true, а значит null это 0.
oleksiiostapets
13.09.2017 07:35-2let x = NaN;
console.log(x == x); // falseOpenA
13.09.2017 11:48NaN — это псевдочисло, любые арифметические операции с которымм всегда возвращают NaN, а операции сравнения всегда возвращают false.
Что бы проверить NaN на NaN, надо использовать функцию «isNaN()»:
let x = 1 * undefined; // NaN isNaN(x); // true (x.toString() === "NaN"); // true
brzsmg
13.09.2017 09:10var mathTest1 = (null >= 0) ? true : false; var mathTest2 = ((null > 0) || (null == 0)) ? true : false; if(mathTest1 == mathTest2){ console.log('INFO: Все в порядке с логикой.'); }else if(mathTest1 != mathTest2){ console.log('WARNING: С логикой проблемы!'); }else{ console.log('ERROR: Что такое логика?'); }
РезультатWARNING: С логикой проблемы!OpenA
13.09.2017 12:31Лично мое мнение, что это просто побочный эффект от преобразований, так как операторами > | >= | <= | < предпологается сравнение только чисел и больше ничего. Поэтому когда мы ими сравниваем:
+[] // 0 [] >= 0 // true +null // 0 null >= 0 // true +{} // NaN ({}) >= 0 // false +undefined // NaN undefined >= 0 // false
рантайм вначале берет и преобразовывает все в числа (преобразовывает так, как эти самые преобразования прописаны для каждого типа), потом сравнивает и выдает нам результат которому мы удивляемся.
Операторы == | === | != | !== в свою очередь предназначены для различных типов, поэтому преобразований не происходит:
null != 0 // true null !== 0 // true
a5ter0id
13.09.2017 14:14+1Понравилось лаконичное объяснение этой застарелой фитчи с сайта learn.javascript.ru:
Некорректный результат сравнения null с 0
faxse
13.09.2017 14:14null – это отсутствие значения, т.е. это не 0, т.к. 0 – это все же значение, поэтому, для меня, null == 0 -> false – это логично.
У Флэнагана есть интересная фраза:
Finally, note that the <= (less than or equal) and >= (greater than or equal) operators do
not rely on the equality or strict equality operators for determining whether two values
are “equal.” Instead, the less-than-or-equal operator is simply defined as “not greater
than,” and the greater-than-or-equal operator is defined as “not less than.
Т.е. можно представить себе, что >= и <= это не математические операторы, а свои JS’ие (со своими правилами), которые ни как не связаны с ==. Тогда опять же все становится логично:
0 < null -> false => 0 >= null -> true
0 > null -> false => 0 <= null -> true
Kusado
— О великий JavaScript, будет ли зарплата в этом месяце?
— !((null>0 | null==0) & null>=0)
BubaVV
(+[![]]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]
ArVaganov
Lol. А существует генератор для подобных вещей?
BubaVV
http://www.jsfuck.com/
ReklatsMasters
Вот ещё один http://utf-8.jp/public/aaencode.html
Alumix
Консоль MS Edge это не осилила, пришлось убивать процесс
IDLester
На автомате скопировал ваш пример в консоль, сразу же заметил ещё одну странную особенность:
Не силён в js, но видимо и этому есть разумное объяснение.
mayorovp
Конечно же есть, и очень простое.
&
— это не булева операция, а численная; булева пишется как&&
.Kusado
То есть в моём примере мы ещё и неявно кастуем из int в bool?
И в случае с !(((null>0 | null==0) & null>=0) + 1) мы зарплату не получим?
RidgeA
потому что там используются битовые операторы
В первом варианте получается
а во втором
IDLester
Во втором видимо всё же:
Исходя из первого примера. Но сути не меняет, спасибо за полное пояснение, как говорится век живи — век учись.
RidgeA
да, так правильнее