Как понять числа с плавающей запятой в JS?

JavaScript

Эта статья была опубликована сообществом cloud+community

Я считаю, что каждый сталкивался с проблемой ошибки арифметической точности с плавающей запятой в обычной разработке JavaScript, такой какconsole.log(0.1+0.2===0.3)// false. В JavaScript все числа, включая целые и десятичные дроби, записываются с помощьюNumberтип для представления. Этот документ представляетNumberстандарт двоичного хранилища для понимания проблем точности арифметических операций с плавающей запятой и пониманияNumberобъектMAX_VALUEКак получить значение атрибута, такого как значение, и, наконец, ввести некоторые часто используемые арифметические решения с точностью с плавающей запятой.

Стандарт хранения номера

Число JavaScript представлено 64-битным типом с плавающей запятой двойной точности, определенным IEEE 754. Конкретное распределение байтов можно сначала посмотреть на картинке, приведенной из Википедии:

img

Как видно из рисунка выше, от старших к младшим 64 бита делятся на 3 сегмента, а именно:

  • знак: бит знака, занимающий 1 бит;
  • экспонента: бит экспоненты, составляющий 11 бит;
  • дробь: значащие цифры, занимающие 52 цифры.

Биты экспоненты имеют 11 бит и диапазон значений от 0 до 2047. Когда бит экспоненты e=0 или e=2017, он имеет разные специальные значения в зависимости от того, равен ли значащий разряд f 0, как показано в следующей таблице:

img

Для обычно используемого нормального числа, чтобы удобно представить случай, когда показатель степени отрицательный, делается смещение -1023 для размера битового значения показателя степени. Для ненулевого числа его первая значащая цифра в двоичном экспоненциальном представлении фиксируется как 1. Таким образом, значение числа с плавающей запятой двойной точности равно

img

Для субнормальных чисел его можно использовать для представления чисел, близких к 0. Его особое место заключается в том, что значащим цифрам предшествует 0 вместо 1, а показатель степени смещен на -1022, поэтому значение равно:

img

Несколько значений свойств в объекте Number

Зная, как хранится число, становится ясно, как свойства объекта числа получают значения.

Number.MAX_VALUE: максимальное число, которое может быть представлено. Очевидно, что когда e и f являются самыми большими, число, которое может быть представлено, является наибольшим. Значение равно

img

Number.MIN_VALUE: наименьшее представимое положительное число, представленное наименьшим субнормальным числом. Когда e = 0, последняя цифра f равна 1, а другая — 0, минимальное значение равно

img

Number.EPSILON : представляет разницу между 1 и наименьшим числом с плавающей запятой больше 1, которое может представлять число. значение

img

Number.MAXSAFEINTEGER: представляет наибольшее безопасное целое число в JavaScript. Целые числа, которые могут быть представлены непрерывно и точно, называются безопасными целыми числами.Например, 2^54 не является безопасным целым числом, потому что оно точно такое же, как 2^54+1, e=1077, f=0.Math.pow(2,54)===Math.pow(2,54)+1// true. После преобразования целого числа в двоичное числа после запятой не будет цифр, но при выражении в двоичном экспоненциальном представлении зарезервировано не более 52 цифр после запятой, плюс ведущая 1, остается 53 цифры, поэтому, когда число преобразуется в двоичный код, если количество цифр превышает 53, конечная часть неизбежно будет усечена, а это означает, что она не может быть точно представлена, что является небезопасным целым числом. Таким образом, наименьшее усеченное целое число равно 100...001=2^53+1 (с 52 нулями между ними). Если это число установлено равным X, то целые числа, меньшие X, могут быть точно представлены, плюс условие «непрерывности», поэтому X-1 — это не тот ответ, который нам нужен, а X-2.Number.MAX_SAFE_INTEGERОкончательное значение

img

Number.MINSAFEINTEGER: представляет наименьшее безопасное целое число в JavaScript, правильноNumber.MAX_SAFE_INTEGERВозьмите отрицательное значение, значение равно -9007199254740991.

Почему 0,1+0,2 не равно 0,3

посмотри сейчасconsole.log(0.1+0.2===0.3)// falseДля этой задачи число 0,1, преобразованное в двоичное, равно 0,0001100110011... т.е. 1,10011001...1001 * 2^-4 (дробная часть имеет 52 бита, то есть 13 1001 тактов). Поскольку 53-й бит равен 1, аналогично десятичному округлению, двоичное значение является «округлением до нуля», поэтому окончательное представление двоичного экспоненциального представления 0,1 равно 1,10011001...1010 * 2^-4, то есть размер двоичного значения равен на самом деле 0,000110011001...10011010. Следующий код проверяет это значение (в напечатанном значении удален завершающий 0):

var a = 0.1;console.log(a.toString(2)); //0.0001100110011001100110011001100110011001100110011001101

Точно так же конечное значение десятичного числа 0,2, преобразованного в двоичное, равно 1,10011001...1010 * 2^-3 или 0,00110011...100111010, окончательное значение десятичного числа 0,3, преобразованного в двоичное, равно 1,00110011...0011 * 2^-2

var b = 0.2;console.log(b.toString(2)); //0.001100110011001100110011001100110011001100110011001101var c = 0.3;console.log(c.toString(2)); //0.010011001100110011001100110011001100110011001100110011

Следовательно, значение 0,1+0,2 представляет собой сложение двоичных значений, соответствующих 0,1 и 0,2 выше, как показано на следующем рисунке.

img

На рисунке выше для полученной суммы «округление нуля» до 52 значащих знаков после запятой является окончательным значением: 0,01001100...110100 (53-й бит равен 1, поэтому он идет вперед на 1), как показано в следующем коде. . Это значение значительно отличается от окончательного двоичного представления 0,3 выше, что объясняет фундаментальную причину, по которой 0,1 + 0,2 не равно 0,3 (на самом деле это значение примерно равно 0,3000000000000000004 в десятичном виде). Примечание. Напечатанная длина равна 54, поскольку имеется 52 значащих десятичных знака, передняя часть равна «0,01», длина равна 4, а последние 2 0 удаляются, поэтому окончательная напечатанная длина составляет 52 + 4-2 = 54.

var d = 0.1 + 0.2;console.log(d.toString(2)); //0.0100110011001100110011001100110011001100110011001101console.log(d.toString(2).length); // 54

Арифметическое решение точности с плавающей запятой

Что касается потери точности в операциях js с плавающей запятой, разные сценарии могут иметь разные решения. 1. Если он используется только для отображения результата числа с плавающей запятой, вы можете позаимствовать методы toFixed и parseFloat объекта Number. В следующем фрагменте кода фиксированный параметр указывает, что необходимо сохранить несколько знаков после запятой, а точность можно отрегулировать в соответствии с реальной сценой.

function formatNum(num, fixed = 10) {    return parseFloat(a.toFixed(fixed))}var a = 0.1 + 0.2;console.log(formatNum(a)); //0.3

2. Если вам нужно выполнять такие операции, как сложение, вычитание, умножение и деление чисел с плавающей запятой, из приведенного выше видно, что целые числа меньше диапазона числа. MAXSAFEINTEGER могут быть точно представлены, поэтому вы можете сначала преобразовать десятичные числа в целые числа, а затем преобразовать после получения результата для соответствующего десятичного числа. Например, сложение двух чисел с плавающей запятой:

 function add(num1, num2) {  var decimalLen1 = (num1.toString().split('.')[1] || '').length; //第一个参数的小数个数  var decimalLen2 = (num2.toString().split('.')[1] || '').length; //第二个参数的小数个数  var baseNum = Math.pow(10, Math.max(decimalLen1, decimalLen2));  return (num1 * baseNum + num2 * baseNum) / baseNum;}console.log(add(0.1 , 0.2)); //0.3

использованная литература

Эта статья была разрешена автором для публикации Tencent Cloud + Community