JavaScript имеет два нуля: -0 и +0. Этот пост объясняет, почему это так и где это имеет значение на практике.
Подписанный ноль
Числа всегда должны быть закодированы для хранения в цифровом виде. Почему некоторые кодировки имеют два нуля? В качестве примера давайте рассмотрим целые числа кодирования в виде 4-значных двоичных чисел с помощью
метода знака и величины . Там используется один бит для знака (0, если положительный, 1, если отрицательный), а остальные биты для
величины (абсолютное значение). Следовательно, -2 и +2 кодируются следующим образом.
Двоичный 1010 десятичный -2
Двоичный 0010 десятичный +2
Естественно, это означает, что будет два нуля: 1000 (-0) и 0000 (+0).
В JavaScript все числа являются числами с плавающей запятой, закодированными с двойной точностью в соответствии со стандартом IEEE 754 для арифметики с плавающей запятой. Этот стандарт обрабатывает знак способом, аналогичным кодированию знака и величины для целых чисел, и поэтому также имеет нулевой знак . Всякий раз, когда вы представляете число в цифровом виде, оно может стать настолько маленьким, что его невозможно отличить от 0, потому что кодировка недостаточно точна, чтобы представить разницу. Затем ноль со знаком позволяет вам записать, «с какого направления» вы приблизились к нулю, какой знак имел номер до того, как он считался нулевым. Википедия хорошо подводит итог плюсы и минусы подписанных нулей :
Утверждается, что включение нуля со знаком в IEEE 754 значительно облегчает достижение численной точности в некоторых критических задачах, в частности, при вычислениях со сложными элементарными функциями. С другой стороны, концепция подписанного нуля противоречит общему предположению, сделанному в большинстве математических областей (и в большинстве курсов математики), что отрицательный ноль — это то же самое, что и ноль. Представления, которые допускают отрицательный ноль, могут быть источником ошибок в программах, поскольку разработчики программного обеспечения не осознают (или могут забыть), что, хотя два нулевых представления ведут себя как равные при числовых сравнениях, они представляют собой разные битовые комбинации и дают в некоторых результатах разные результаты. операции.
JavaScript идет на все, чтобы скрыть тот факт, что есть два нуля.
Скрытие знака нуля
В JavaScript вы обычно пишете 0, что всегда означает +0. Но он также отображает -0 просто как 0. Далее вы видите то, что вы видите, когда используете любую командную строку браузера или Node.js REPL:
> -0 0
Причина в том, что стандартный метод toString () преобразует оба ноля в один и тот же «0».
> (-0).toString() '0' > (+0).toString() '0'
Иллюзия одиночного нуля также совершается операторами равенства. Даже при строгом равенстве оба ноля считаются одинаковыми, что затрудняет их разделение (в том редком случае, когда вы хотите).
> +0 === -0 true
То же самое верно для операторов «меньше» и «больше» — они считают оба ноля равными.
> -0 < +0 false > +0 < -0 false
Где знак нуля имеет значение
Знак 0 редко влияет на результаты вычислений. И +0 является наиболее распространенным 0. Только несколько операций выдают -0, большинство из них просто пропускают существующий -0. В этом разделе показано несколько примеров, где знак имеет значение. Для каждого примера подумайте, можно ли его использовать для разделения -0 и +0 — задачи, которую мы будем решать в разделе. 4. Чтобы сделать знак ноля видимым, мы используем следующую функцию.
function signed(x) { if (x === 0) { // isNegativeZero() will be shown later return isNegativeZero(x) ? "-0" : "+0"; } else { // Otherwise, fall back to the default // We don’t use x.toString() so that x can be null or undefined return Number.prototype.toString.call(x); } }
Добавление нулей
Цитировать раздел 11.6.3 спецификации ECMAScript 5.1 «Применение аддитивных операторов к числам»:
Сумма двух отрицательных нулей равна -0. Сумма двух положительных нулей или двух нулей противоположного знака равна +0.
Например:
> signed(-0 + -0) '-0' > signed(-0 + +0) '+0'
Это не дает вам возможности различить два нуля, потому что то, что получается, так же трудно отличить, как и то, что входит.
Умножение на ноль
При умножении на ноль с конечным числом применяются обычные правила знака:
> signed(+0 * -5) '-0' > signed(-0 * -5) '+0'
Умножение бесконечности на ноль приводит к NaN:
> -Infinity * +0 NaN
Деление на ноль
Вы можете разделить любое ненулевое число (включая бесконечности) на ноль. Результатом является бесконечность, знак которой подчиняется обычным правилам.
> 5 / +0 Infinity > 5 / -0 -Infinity > -Infinity / +0 -Infinity
Обратите внимание, что -Infinity и + Infinity можно различить через ===.
> -Infinity === Infinity false
Разделение нуля на ноль приводит к NaN:
> 0 / 0 NaN > +0 / -0 NaN
Math.pow ()
Ниже приведена таблица результатов Math.pow (), если первый аргумент равен нулю:
пау (+0, у <0)? +8
Пау (-0, нечетное у <0)? -8
Пау (-0, даже у <0)? +8
Взаимодействие:
> Math.pow(+0, -1) Infinity > Math.pow(-0, -1) -Infinity
Math.atan2 ()
Ниже приведена таблица результатов, которые возвращаются, если один из аргументов равен нулю.
atan2 (+0, +0)? +0
atan2 (+0, -0)? + р
атан2 (-0, +0)? -0
атан2 (-0, -0)? -p
atan2 (+0, x <0)? + р
атан2 (-0, х <0)? -п
Следовательно, есть несколько способов определить знак нуля. Например:
> Math.atan2(-0, -1) -3.141592653589793 > Math.atan2(+0, -1) 3.141592653589793
atan2 — одна из немногих операций, которая выдает -0 для ненулевых аргументов:
atan2 (y> 0, +8)? +0
atan2 (y <0, +8)? -0
Следовательно:
> signed(Math.atan2(-1, Infinity)) '-0'
Math.round ()
Math.round () — это еще одна операция, которая возвращает -0 для аргументов, отличных от -0 и +0:
> signed(Math.round(-0.1)) '-0'
Здесь мы имеем эффект, о котором мы говорили в начале: знак нуля записывает знак значения перед округлением, «с какой стороны» мы приблизились к 0.
Различение двух нулей
Каноническое решение для определения знака нуля состоит в том, чтобы разделить его на единицу и затем проверить, равен ли результат -Infinity или + Infinity:
function isNegativeZero(x) { return x === 0 && (1/x < 0); }
Вышеуказанные разделы показали несколько других вариантов. Одно
оригинальное решение исходит от Аллена Вирфса-Брока. Вот немного измененная версия этого:
function isNegativeZero(x) { if (x !== 0) return false; var obj = {}; Object.defineProperty(obj, 'z', { value: -0, configurable: false }); try { // Is x different from z’s previous value? Then throw exception. Object.defineProperty(obj, 'z', { value: x }); } catch (e) { return false }; return true; }
Объяснение: Как правило, вы не можете переопределить ненастраиваемое свойство — будет сгенерировано исключение. Например:
TypeError: Cannot redefine property: z
Однако JavaScript будет игнорировать вашу попытку, если вы используете то же значение, что и существующее. В этом случае одно и то же значение определяется не через ===, а через внутреннюю операцию, которая различает -0 и +0. Вы можете прочитать на детали в Wirfs-Брока
блоге (замораживание объекта делает все свойства не конфигурируемый).
Вывод
Мы видели, что есть два нуля из-за того, как знак кодируется для чисел JavaScript. Однако, -0 обычно скрыт, и лучше всего делать вид, что есть только один ноль. Тем более, что разница между нулями мало влияет на вычисления. Даже строгое равенство не может отличить их друг от друга. Если вам, вопреки всем ожиданиям или просто для удовольствия, нужно определить знак нуля, есть несколько способов сделать это. Обратите внимание, что немного странное существование двух нулей не является ошибкой JavaScript, оно просто следует стандарту IEEE 754 для чисел с плавающей запятой.
Связанное чтение
Этот пост является частью серии о числах JavaScript, которая включает следующие посты:
Кроме того, в блоге «
Более строгое равенство в JavaScript » говорится, что === не может обнаружить ни значение NaN, ни знак нуля.