Как мы все знаем, операции с числами с плавающей запятой в JavaScript часто0.000000001
а также0.999999999
Такие странные результаты, как0.1+0.2=0.30000000000000004
,1-0.9=0.09999999999999998
, многие знают, что это проблема с ошибками с плавающей запятой, но это не ясно. Эта статья поможет вам прояснить принципы и решения, стоящие за этим, а также объяснит кризис больших чисел в JS и ямы, с которыми вы столкнетесь в четырех арифметических операциях.
Хранилище с плавающей запятой
Первое, что нужно сделать, это выяснить, как JavaScript хранит дебилисты. В отличие от других языков, таких как Java и Python, все номера в JavaScript, включая целые числа и десятичные числа, имеют только один тип -Number
. Его реализация следуетIEEE 754Стандартный, использующий 64-битное представление с фиксированной длиной, то есть стандартные числа с плавающей запятой двойной двойной точности (связанные с плавающими 32-битными числами одинарной точности). Подробно освещены принципы компьютерной композиции, если не помните, ничего страшного.
Преимущество такой структуры хранения заключается в том, что целые и десятичные числа можно нормализовать для экономии места для хранения.
64-битные биты можно разделить на три части:
- Знаковый бит S: первый бит — это знаковый бит (знак) положительных и отрицательных чисел, 0 для положительных чисел и 1 для отрицательных чисел.
- Бит экспоненты E: Средние 11 бит хранят экспоненту (экспоненту), которая используется для представления мощности.
- Мантисса M: последние 52 бита — это мантисса, а лишняя часть автоматически округляется до нуля.
Фактическое число может быть рассчитано по следующей формуле:

$ V = (-1)^{S}\times M \times 2^{E} $
Обратите внимание, что приведенная выше формула следует спецификации экспоненциального представления, которое составляет 0E = 1025
, мантисса М равна 001.
Окончательная формула становится:
$ V = (-1)^{S}\times (M+1) \times 2^{E-1023} $
так4.5
Окончательное представление (M=001, E=1025):

(изображение создано из этогоwoohoo.binary convert.com/convert_both…)
Следующий0.1
Пример, объясняющий причину ошибки с плавающей запятой,0.1
Преобразуется в двоичное представление как0.0001100110011001100
(1100 циклов),1.100110011001100x2^-4
,такE=-4+1023=1019
;M отбрасывает ведущую 1, чтобы получить100110011...
. В итоге это:

Преобразуется в десятичную как0.100000000000000005551115123126
, поэтому возникает ошибка с плавающей запятой.
Зачем0.1+0.2=0.30000000000000004
?
Этапы расчета:
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
// 转成十进制正好是 0.30000000000000004
Зачемx=0.1
может получить0.1
?
Поздравляю, вы достигли области, где вы можете видеть, что гора не гора. Поскольку фиксированная длина мантиссы составляет 52 бита плюс пропущенный один бит, максимальное число, которое может быть представлено, равно2^53=9007199254740992
, соответствующая мантисса в экспоненциальном представлении равна9.007199254740992
, что является максимальной точностью, которую может представить JS. Его длина 16, поэтому вы можете использоватьtoPrecision(16)
Для выполнения точных операций избыточная точность будет автоматически округлена в большую сторону. Итак, есть:
0.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉末尾的零后正好为 0.1
// 但你看到的 `0.1` 实际上并不是 `0.1`。不信你可用更高的精度试试:
0.1.toPrecision(21) = 0.100000000000000005551
кризис больших чисел
Может быть, у вас есть смутное ощущение, что произойдет, если целое число больше 9007199254740992?
Поскольку максимальное значение E равно 1023, наибольшее целое число, которое может быть представлено, равно2^1024 - 1
, что является наибольшим целым числом, которое может быть представлено. Но вы не можете вычислить число таким образом, потому что из2^1024
стал в началеInfinity
> Math.pow(2, 1023)
8.98846567431158e+307
> Math.pow(2, 1024)
Infinity
тогда для(2^53, 2^63)
Что произойдет с числами между ними?
-
(2^53, 2^54)
Число между выберет одно из двух, только четные числа могут быть представлены точно -
(2^54, 2^55)
Число между ними выберет одно из четырех и может представлять ровно 4 кратных. - ... пропустить больше кратных 2 по очереди
Следующая картинка хорошо иллюстрирует соответствие между числами с плавающей запятой и действительными числами в JavaScript. наш обычный(-2^53, 2^53)
Это лишь очень маленькая часть посередине, и тем более разреженная и неточная она с двух сторон.
В ранней системе заказов Taobao номер заказа рассматривался как число, а затем номер случайного заказа резко увеличился, что превысило
9007199254740992
, окончательное решение состоит в том, чтобы преобразовать номер заказа в строку для обработки.
Для решения проблемы больших чисел можно обратиться к сторонней библиотекеbignumber.js, принцип состоит в том, чтобы рассматривать все числа как строки и заново реализовывать логику вычислений.Недостаток в том, что производительность намного хуже, чем нативная. Поэтому необходима встроенная поддержка больших чисел, и теперь у TC39 уже есть предложение Stage 3.proposal bigint, Большинство проблем были полностью решены.
toPrecision
vs toFixed
Обработка данных, легко запутать эти две функции. Общий знаменатель состоит в том, чтобы превратиться в строку чисел для отображения. Будьте осторожны, чтобы не использовать в середине процесса расчета, только для окончательного результата.
Необходимо отметить отличия:
-
toPrecision
— точность обработки, и точность отсчитывается от первого ненулевого числа слева направо. -
toFixed
Округляется до указанного количества знаков после запятой, начиная с запятой.
Оба могут округлять лишние числа, и некоторые люди используютtoFixed
сделать округление, но обязательно знайте, что в нем есть ошибки.
Такие как:1.005.toFixed(2)
то, что возвращается1.00
вместо1.01
.
причина:1.005
Фактическая соответствующая цифра1.00499999999999989
, которые все округляются при округлении!
Решение: используйте профессиональную функцию округления.Math.round()
обрабатывать. ноMath.round(1.005 * 100) / 100
еще нет, потому что1.005 * 100 = 100.49999999999999
. Также необходимо решить ошибки точности умножения и деления перед использованиемMath.round
. Вы можете использовать следующиеnumber-precision#round
метод решения.
решение
Вернемся к самому волнующему вопросу: как решать ошибки с плавающей запятой. Прежде всего, теоретически невозможно хранить бесконечные десятичные дроби в ограниченном пространстве, чтобы быть точным, но мы можем справиться с этим, чтобы получить ожидаемые результаты.
Класс отображения данных
когда вы получаете1.4000000000000001
Когда такие данные должны быть отображены, рекомендуется использоватьtoPrecision
округлятьparseFloat
Преобразуйте его в число, а затем отобразите следующим образом:
parseFloat(1.4000000000000001.toPrecision(12)) === 1.4 // True
скопировать код
Метод инкапсуляции:
function strip(num, precision = 12) {
return +parseFloat(num.toPrecision(precision));
}
Почему вы выбираете12
Как точность по умолчанию? Это эмпирический выбор. Как правило, выбирая 12 может решить большую часть проблем 0001 и 0009, и в большинстве случаев достаточно. Если вам нужно больше точности, вы можете увеличить его.
Тип операции с данными
Для арифметических операций, таких как+-*/
, вы не можете использоватьtoPrecision
. Правильный способ - преобразовать десятичную дробь в целое число, а затем действовать. В качестве примера возьмем дополнение:
/**
* 精确加法
*/
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
Описанный выше метод применим к большинству сценариев. столкнуться с научной нотацией, такой как2.3e+1
(Когда числовая точность превышает 21, число будет принудительно отображаться в экспоненциальном представлении) и требуется особая обработка.
Я могу прочитать это здесь, указывая, что вы очень терпеливы, тогда я поставлю пособие. Может использоваться непосредственно, когда вы сталкиваетесь с проблемой ошибки с плавающей запятой
Github.com/ А вот он — Share/Num Be…
Отлично поддерживает сложение, вычитание, умножение и деление чисел с плавающей запятой, округление и другие операции. Очень маленький, всего 1 КБ, намного меньше, чем большинство подобных библиотек (таких как Math.js, BigDecimal.js), 100% полное тестовое покрытие, хорошая читабельность кода, вы также можете использовать его в своем приложении!
Ссылаться на
- What Every Programmer Should Know About Floating-Point Arithmetic
- Why Computers are Bad at Algebra | Infinite Series
- Is Your Model Susceptible to Floating-Point Errors?
- IEEE 754
Конечно, этот пост написан для рекрутинга! ! !
Отдел обработки больших данных Alibaba искренне набирает осадных львов. Не медлите, вдруг пройдет.
Отправьте свое резюмеneosoyn@gmail.com