Первоначально из колонки Чжиху:zhuanlan.zhihu.com/ne-fe
Как мы все знаем, операции с числами с плавающей запятой в JavaScript часто0.000000001
а также0.999999999
Такие странные результаты, как0.1+0.2=0.30000000000000004
,1-0.9=0.09999999999999998
Многие люди знают, что это проблема с плавающей точкой, но конкретная причина не ясна. Эта статья поможет вам уточнить принципы и решения позади этого, и объяснит ямы, которые встретятся в JS в JS.
хранение чисел с плавающей запятой
Первое, что нужно сделать, это выяснить, как JavaScript хранит десятичные дроби. В отличие от других языков, таких как Java и Python, все числа в JavaScript, включая целые и десятичные дроби, имеют только один тип —Number
. Его реализация следуетIEEE 754Стандартный, использующий 64-битное представление с фиксированной длиной, то есть стандартные числа с плавающей запятой двойной двойной точности (связанные с плавающими 32-битными числами одинарной точности). Подробно освещены принципы компьютерной композиции, если не помните, ничего страшного.
Примечание: десятичные дроби в большинстве языков по умолчанию соответствуют числам с плавающей запятой, совместимым с IEEE 754, включая Java, Ruby и Python.Ошибки с плавающей запятой, описанные в этой статье, также существуют.
Преимущество такой структуры хранения заключается в том, что целые и десятичные числа можно нормализовать для экономии места для хранения.
64-битные биты можно разделить на три части:
- Знаковый бит S: первый бит — это знаковый бит (знак) положительных и отрицательных чисел, 0 для положительных чисел и 1 для отрицательных чисел.
- Бит экспоненты E: Средние 11 бит хранят экспоненту (экспоненту), которая используется для представления мощности.
- Мантисса M: последние 52 бита — это мантисса, а лишняя часть автоматически округляется до нуля.
Фактическое число может быть рассчитано по следующей формуле:
Обратите внимание, что приведенная выше формула соответствует норме научной записи в десятичном формате.0<M<10
, в двоичном виде0<M<2
. То есть целая часть может быть только 1, поэтому ее можно округлить, и останется только дробная часть. Такие как4.5
Преобразованный в двоичный файл100.1
, в научной записи1.001*2^2
, после округления 1M = 001
. E是一个无符号整数,因为长度是11位,取值范围是 0~2047。但是科学计数法中的指数是可以为负数的,所以约定减去一个中间数 1023,[0,1022]
представлен как отрицательный,[1024,2047]
выражен как положительный. показатели вроде 4,5E = 1025
, мантиссаM = 001
.
Окончательная формула становится:
так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 уже есть предложение Этапа 3proposal bigint, ожидается, что большое количество проблем будет полностью решено. Прежде чем браузер официально поддержит его, его можно реализовать с помощью Babel 7.0, который автоматически конвертируется вbig-integerДля расчета это может сохранить точность, но эффективность работы будет снижена.
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/but he-share/num be…
Отлично поддерживает сложение, вычитание, умножение и деление чисел с плавающей запятой, округление и другие операции. Очень маленький, всего 1 КБ, намного меньше, чем большинство подобных библиотек (таких как Math.js, BigDecimal.js), 100% полное тестовое покрытие, хорошая читабельность кода, вы также можете использовать его в своем приложении!
Ссылаться на
- Double-precision floating-point format
- 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?
Если вы считаете, что эта статья поможет вам, коснитесь лайка, чтобы поощрить ее.