Однажды я собирал материалы чтобы устроить ликбез по 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)


  1. Kusado
    12.09.2017 14:49
    +7

    — О великий JavaScript, будет ли зарплата в этом месяце?
    — !((null>0 | null==0) & null>=0)


    1. BubaVV
      12.09.2017 15:01
      +5

      (+[![]]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]


      1. ArVaganov
        12.09.2017 16:08

        Lol. А существует генератор для подобных вещей?



        1. ReklatsMasters
          12.09.2017 18:20
          +1

      1. Alumix
        12.09.2017 19:00

        Консоль MS Edge это не осилила, пришлось убивать процесс


    1. IDLester
      12.09.2017 15:11
      -1

      На автомате скопировал ваш пример в консоль, сразу же заметил ещё одну странную особенность:

      !((null>0 | null==0) & null>=0) // true
      (!(null>0 | null==0) & null>=0) // 1

      Не силён в js, но видимо и этому есть разумное объяснение.


      1. mayorovp
        12.09.2017 15:15
        +4

        Конечно же есть, и очень простое. & — это не булева операция, а численная; булева пишется как &&.


        1. Kusado
          12.09.2017 15:19
          +2

          То есть в моём примере мы ещё и неявно кастуем из int в bool?
          И в случае с !(((null>0 | null==0) & null>=0) + 1) мы зарплату не получим?


      1. RidgeA
        12.09.2017 15:17
        +1

        потому что там используются битовые операторы
        В первом варианте получается

        !((0 | 0) & 1) => !(0 & 1) => !(0) => true
        

        а во втором
        (!(0 | 0) & 1) => (!(0) & 1) => (1 & 1) => 1
        


        1. IDLester
          12.09.2017 15:27

          Во втором видимо всё же:

          (!(0 | 0) & 1) => (!(0) & 1) => (true & 1) => 1

          Исходя из первого примера. Но сути не меняет, спасибо за полное пояснение, как говорится век живи — век учись.


          1. RidgeA
            12.09.2017 16:11

            да, так правильнее


  1. kalininmr
    12.09.2017 15:42
    +4

    казалось бы.
    > Если null < 0 принимает значение false, то null >= 0 принимает значение true
    тут все логично

    получается всю математическую логику ломает странный ==


    1. khim
      12.09.2017 18:27
      +3

      Там (так же, как и в PHP) всё странное. Для многих алгиритмов нужно отношение порядка или отношение эквивалентности. Ни того, ни другого ни PHP, ни в JavaScript по умолчанию нету — что совершенно логично (эксивалентность достигается с помощью ===, а порядок… нету порядка).

      Дело в том, что для того, что операторы сравнения работали разумно в нетипизированных языках нужно не два результата, а три: true, false, unorderable types.

      Но поскольку PHP и JavaScript писались не для программистов, то было принято решение — программа должна работать любой ценой! Великолепное Job Security, так как без навешивания костылей (типа компилятора с TypeScript или ClojureScript) ошибки можно искать вечно…


      1. kalininmr
        12.09.2017 21:09

        впринципе то понятно, null == 0 — False вполне логично… хотя в С будет True :)


        1. CyberSoft
          12.09.2017 21:33

          Логично там, где есть разделение на ссылочные и примитивы. А в С null задефайнен на 0, и то не факт


          1. kalininmr
            13.09.2017 01:39

            ну да. раньше был просто дефайн сейчас неуверен.


  1. Aberro
    12.09.2017 15:54
    +4

    «И, на самом деле, в этом есть смысл. С точки зрения математики, если у нас есть два числа, x и y, и если x не меньше, чем y, тогда x должно быть больше чем y или равно ему.»
    Если нельзя вернуть неопределённый результат, а нужно вернуть только true или false, то как насчёт векторов, матриц? Если один вектор не меньше другого, это ведь не значит, что он больше или равен, это значит, что он не меньше, не больше, не равен и вообще не подлежит ранжированию. Так что с точки зрения математики определять один оператор через результат другого — глупо.


    1. MacIn
      12.09.2017 16:42

      Просто в вашем случае область определения функции сравнения иная — есть еще дискретный «некорректное значение». А здесь область определения чисто булева, так что все корректно.


      1. mayorovp
        12.09.2017 17:06

        Область значений, а не определения.


        1. MacIn
          12.09.2017 17:16

          Разумеется.


      1. 0xd34df00d
        13.09.2017 21:46

        Эм, нет. В том случае задан частичный порядок (хотя можно задать и полный, но он, по крайней мере, для матриц не будет согласован с умножением, что не очень приятно), а на вещественных числах таки есть полный порядок, согласованный с операциями на этом кольце.


        А в математике отношение «меньше» на множестве A — это именно что бинарное отношение, то есть подмножество множества (A x A). Соответственно «x больше чем y» следует из «y меньше чем x», а не из «x не меньше чем y» по описанной в исходном комментарии причине.


  1. kahi4
    12.09.2017 16:05
    +2

    RelationalExpression : 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). В статье так и написано, но в переводе спецификации как-то странно, может в старой версии так и было.


  1. anfilat
    12.09.2017 16:29
    +2

    Авторы стандарта так и не решили для себя окончательно, что такое null — особый тип объекта или особый тип нуля, вот и получился баг в спецификации


  1. pm_wanderer
    12.09.2017 18:38
    -4

    Смотрящим за стандартом ES давно пора пометить undefined и null как obsolete и заменить на условный nil — «нет значения».
    А по-хорошему вообще ввести новый режим браузера, типа «use superstrict»: оставить там все лучшее и выкинуть устаревшее.


    1. kuraga333
      13.09.2017 07:35
      +5

      Пожалуй, за undefined как раз и стоит похвалить JavaScript…


      1. pm_wanderer
        13.09.2017 11:47
        +1

        У него семантический смысл — переменной еще небыло присвоено значение.
        Следовательно, не совсем корректно undefined присваивать самостоятельно.
        Я считаю, что надо ввести новое примитивное значение nil, семантический смысл которого — «просто нет значения». Nil будет унифицировать синтаксис и сочетать в себе лучшее от undefined и null. Сейчас по сути это и так происходит в TypeScript — при компиляции там не используется null и все решается через undefined. Я лишь предлагаю другое имя)


        1. mayorovp
          13.09.2017 12:46

          Сейчас по сути это и так происходит в TypeScript — при компиляции там не используется null и все решается через undefined.

          А можно пруф?


          1. pm_wanderer
            13.09.2017 16:11
            +1

            Немного напутал — null не применяется в разработке самого языка:
            null and undefined


        1. 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.


          1. pm_wanderer
            14.09.2017 01:12
            +1

            Именно такое значение по спецификации я имею ввиду — она очень уж узко определяет эти вещи. Думаю в будущем это будет исправлено и унифицировано. Самый простой вариант на данный момент — просто отказаться от null и принять undefined за «просто нет значения», как Вы и считали изначально)


          1. vanxant
            15.09.2017 22:44

            Вообще, конечно, null должен быть «пустым значением» (математики бы назвали единицей относительно операции сложения для данного типа). Числом 0, пустой строкой, массивом из 0 элементов, пустым объектом без свойств. Но при этом — штатным значением, неразрушающим другие значения при участии в операциях, и, главное, типизированным. И не для каждого типа вообще существовать (примеры типов без нуля: boolean, сложные классы типа user и т.д.)
            А undefined — именно что нештатным, нетипизированным и разрушающим, для ошибочных ситуаций.
            К сожалению, при создании первых версий js до этого не додумались, а потом уже было поздно.


  1. Varim
    12.09.2017 20:45
    -3

    И, на самом деле, в этом есть смысл. С точки зрения математики, если у нас есть два числа, x и y, и если x не меньше, чем y, тогда x должно быть больше чем y или равно ему.
    Нелогично. Тренарная логика и базы данных несогласны.
    true, false, null, undefined это 3х или даже 4х арная логика.


  1. 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


    1. vlivyur
      13.09.2017 11:00
      +1

      Ну с математикой там всё сложно, особенно если учесть что null <= 0 тоже true, а значит null это 0.


    1. myrslok
      13.09.2017 13:35

      Это скорее возможное определение, чем осмысленное утверждение.


  1. oleksiiostapets
    13.09.2017 07:35
    -2

    let x = NaN;
    console.log(x == x); // false


    1. mayorovp
      13.09.2017 08:44
      +1

      Вот как раз это было взято не с потолка, а из стандарта.


    1. OpenA
      13.09.2017 11:48

      NaN — это псевдочисло, любые арифметические операции с которымм всегда возвращают NaN, а операции сравнения всегда возвращают false.
      Что бы проверить NaN на NaN, надо использовать функцию «isNaN()»:

      let x = 1 * undefined; // NaN
      
      isNaN(x); // true
      
      (x.toString() === "NaN"); // true
      


      1. vanxant
        15.09.2017 22:46

        isNaN именно так и определяется, через return !(x==x)


        1. OpenA
          16.09.2017 06:44
          -1

          isNaN именно так и определяется, через return !(x==x)
          А что если число вещественное? Они ведь то же с этим оператором не дружат.
          Не помню правда, для одной и той же переменной это справедливо или нет.


  1. paluke
    13.09.2017 07:35
    -3

    Ну бред же. a == b должно быть эквивалентно !(a<b) && !(b<a).


    1. myrslok
      13.09.2017 11:48
      +2

      Должно для чего? Для частично упорядоченных множеств это неверно.


      1. paluke
        14.09.2017 06:53

        Ну ок, неверно. Но тогда и то что a>=b эквивалентно !(a<b) тоже неверно.


        1. myrslok
          14.09.2017 12:29
          +1

          Да, a>=b (понимаемое как a>b || a=b), не эквивалентно !(a<b). Но в данном случае a>=b равно !(a<b) по определению.


  1. brzsmg
    13.09.2017 09:10

    var 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: С логикой проблемы!


  1. 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


  1. a5ter0id
    13.09.2017 14:14
    +1

    Понравилось лаконичное объяснение этой застарелой фитчи с сайта learn.javascript.ru:

    Некорректный результат сравнения null с 0


  1. faxse
    13.09.2017 14:14

    null – это отсутствие значения, т.е. это не 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