JavaScript: загадочное дело выражения null >= 0 +46

- такой же как Forbes, только лучше.



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

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

    • IDLester
      /#10412314 / -1

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

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

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

      • mayorovp
        /#10412320 / +4

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

        • Kusado
          /#10412332 / +2

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

      • RidgeA
        /#10412328 / +1

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

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

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

        • IDLester
          /#10412358

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

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

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

  2. kalininmr
    /#10412382 / +4

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

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

    • khim
      /#10412736 / +3

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

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

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

      • kalininmr
        /#10412948

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

        • CyberSoft
          /#10412976

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

          • kalininmr
            /#10413186

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

  3. Aberro
    /#10412404 / +4

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

    • MacIn
      /#10412526

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

      • mayorovp
        /#10412560

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

      • 0xd34df00d
        /#10415094

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


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

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

  5. anfilat
    /#10412506 / +2

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

  6. pm_wanderer
    /#10412756 / -4

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

    • kuraga333
      /#10413316 / +5

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

      • pm_wanderer
        /#10413656 / +1

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

        • mayorovp
          /#10413820

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

          А можно пруф?

        • kuraga333
          /#10414848

          Для меня 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
            /#10415302 / +1

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

          • vanxant
            /#10418770

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

  7. Varim
    /#10412928 / -3

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

  8. CyberSoft
    /#10413002 / +8

    С точки зрения математики, если у нас есть два числа, x и y, и если x не меньше, чем y, тогда x должно быть больше чем y или равно ему.

    Тут-то всё логично, но раз уж схватились за математику, то этот код:


    null > 0; // false
    null == 0; // false
    null >= 0; // true

    с её точки зрения нарушает очевидное математическое утверждение:


    A >= B это тоже самое, что A > B || A == B

    • vlivyur
      /#10413562 / +1

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

    • myrslok
      /#10413992

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

  9. oleksiiostapets
    /#10413318 / -2

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

    • mayorovp
      /#10413378 / +1

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

    • OpenA
      /#10413660

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

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

      • vanxant
        /#10418774

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

        • OpenA
          /#10419078 / -1

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

  10. paluke
    /#10413320 / -3

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

    • myrslok
      /#10413662 / +2

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

      • paluke
        /#10415400

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

        • myrslok
          /#10415854 / +1

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

  11. brzsmg
    /#10413420

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

  12. OpenA
    /#10413786

    Лично мое мнение, что это просто побочный эффект от преобразований, так как операторами > | >= | <= | < предпологается сравнение только чисел и больше ничего. Поэтому когда мы ими сравниваем:


    +[]         // 0
    [] >= 0     // true
    
    +null       // 0
    null >= 0   // true
    
    +{}         // NaN
    ({}) >= 0   // false
    
    +undefined      // NaN
    undefined >= 0  // false

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


    Операторы == | === | != | !== в свою очередь предназначены для различных типов, поэтому преобразований не происходит:


    null != 0    // true
    null !== 0  // true

  13. a5ter0id
    /#10414104 / +1

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

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

  14. faxse
    /#10414106

    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