Введение Почему 0,1 + 0,2 === 0,300000000000000004, 0,3 - 0,2 === 0,099999999999999998 ?
предисловие
В недавнем развитии бизнеса автор столкнулся с ошибками, связанными с номерами плавающих точек JavaScript.
Вот краткое описание фона: при выводе связанного бизнеса он будет отображаться пользователю以元为单位Значение преобразуется в以分为单位ценность . Например,0.57元 转化为 57 分.
Преобразование легко
// 小程序代码 onInput: 监听Input事件
onInput(e) {
let value = e.target.value;
//限制除数字和小数点以外的字符输入
if (!/^\d*\.{0,2}\d{0,2}$/.test(value)) {
value = value
.replace(/[^\d.]/g, '')
.replace(/^\./g, '')
.replace(/\.{2,}/g, '.')
// 保留数字小数点后两位
.replace(/^(.*\..{2}).*$/, '$1');
}
//...
this.setData({
cash: +value * 100 // 乘100, 将元转化为分
})
}
Этот, казалось бы, не проблемный код, когда он отправляется в фоновом режиме, интерфейс возвращает формат значения параметра в неправильном формате.
Сначала было подозрение, что регулярное выражение было недосмотром, но после его проверки проблем не возникло, а затем я попробовал введенное пользователем значение 0,57, но обнаружил, что вычисленное значение было неожиданным, в чем и заключается вопрос в вопросе0.57 * 100 === 56.99999999999999
Студенты, изучающие фронтенд-разработку, должны были видеть это более или менее0.1 + 0.2 === 0.30000000000000004этот классический вопрос. Автор тоже с любопытным отношением в начале читал соответствующую статью, и мне стыдно об этом говорить, думая, что я все равно никогда ее не разовью0.1 + 0.2бизнес, и только узнал, почему это был такой результат, и это было только дело вкуса.
Теперь, когда я наступил на яму, я могу только сказать, что я прыгнул в яму, вырытую тогда, поэтому сегодня я засыплю яму.
В этой статье будут рассмотрены следующие вопросы, и тем, кто уже знаком с ней, возможно, не нужно ее читать.
- Почему 0,1 + 0,2 === 0,300000000000000004
- Почему 0,57 * 100 === 56,99999999999999
- Почему 0,57*1000 === 570
Why 0.1 + 0.2 === 0.30000000000000004 ?
Чтобы ответить на этот вопрос, мы всегда можем обойти самое основное и ядро JavaScript.浮点数的格式存储. В JS как целые, так и десятичные числаNumberтип, его реализация следуетIEEE 754, является стандартным числом двойной точности с плавающей запятой, использующим фиксированное 64-битное представление.
Вы, вероятно, не хотите смотреть его больше. Ладно, ладно, об этом позже, вот простое объяснение на простом языке, а подробности читайте далее в статье.
На самом деле числа в JS будут преобразованы в двоичные и сохранены, потому что хранилище чисел ограничено 64 битами, но в реальном мире числа бесконечны, поэтому должны быть числа за пределами этого диапазона хранения. Числа за пределами этого диапазона теряют точность при сохранении.
В то же время все мы знаем, что когда целое десятичное число преобразуется в двоичное, оно делится на два и удаляется остаток, который делится! двоичный, дробная часть * 2, взять Целая часть, до дробной части равна 0, если никогда не ноль, 0 округляется до 1, когда он превышает последнюю цифру точности.
/* 0.1 转化为二进制的计算过程 */
0.1 * 2 = 0.2 > 取0
0.2 * 2 = 0.4 > 取0
0.4 * 2 = 0.8 > 取0
0.8 * 2 = 1.6 > 取1
0.6 * 2 = 1.2 > 取1
0.2 * 2 = 0.4 > 取0
...
后面就是循环了
Здесь мы можем найти некоторые подсказки
// 使用toString(2), 将10进制输出为二进制的字符串
0.1.toString(2);
// "0.00011001100110011001100110011001100110011001100110011001100..."
0.2.toString(2);
// "0.001100110011001100110011001100110011001100110011001100110011..."
// 二进制相加结果, 由于超过精度, 取52位, 第53位舍0进1
> "0.010011001100110011001100110011001100110011001100110011,1"
// 最后存储下来的结果是
const s = "0.010011001100110011001100110011001100110011001100110100"
// 用算法处理一下。
a = 0;
s.split('').forEach((i, index) => { a += (+i/Math.pow(2, index+1))});
// a >> 0.30000000000000004
сюда,0.1 + 0.2 === 0.30000000000000004из
Все еще есть некоторые сомнения по поводу приведенного выше обсуждения.
- Почему точность превышает 52 бита после преобразования десятичного числа в двоичное?
Это сКак хранятся 64-битные числа двойной точности с плавающей запятойОб этом мы поговорим в конце.
Why 0.57 * 100 === 56.99999999999999 ?
Why 0.57 * 1000 === 570 ?
После прочтения предыдущего раздела у нас также могут быть некоторые предположения об умножении десятичных дробей.
Когда сохраняется значение 0,57, его точность не очень точна.Мы используемtoPrecisionЭтот метод может получить десятичную точность.
0.57.toPrecision(55)
// "0.5699999999999999511501869164931122213602066040039062500"
Первоначальная идея автора была немного глупой,0.57Фактическая стоимость0.56999.., Это0.57 * 100то есть0.56999... * 100, результат56.99999999999999Ла.
В это время г-н Лу задал мне вопрос, почему0.57 * 1000 === 570вместо569.99999..., могу ответить только "Это должна быть потеря точности"
Однако мои «маленькие глазки наполнились большими сомнениями»…
На самом деле, подумав об этом, мы все знаем, что компьютерное умножение на самом деле является накопительным вычислением, а не побитовым умножением, как мы думаем.
// 伪代码
(0.57) * 100
= (0.57) * (64 + 32 + 4)
= (0.57二进制) * (2^6 + 2^5 + 2^2)
= 0.57二进制 * 2^6 + 0.57二进制 * 2^5 + 0.57 * 2^2
Из-за потери точности это действительно теряется.При преобразовании из двоичного в десятичный результат 56,99999...
По аналогии,(0.57 * 1000)Это не простое умножение, и оно тоже накапливается, но при потере конечной точности 0 округляется до 1, и получается 570.
Решать проблему
Для большинства предприятий после определения цифровой точности используйтеMath.roundВот и все.
Например, ОШИБКА, впервые обнаруженная в этой статье.
const value = Math.round(0.57 * 100);
И мы не уверены в точности операций с числами с плавающей запятой, общее решение состоит в том, чтобы преобразовать десятичные числа в целые после вычисления, а затем преобразовать в десятичные числа.
Далее цитата[1]
/**
* 精确加法
*/
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;
}
Конечно, уже есть зрелые библиотеки инструментов, такие какMath.js, BigDecimal.js, number-precisionПодождите, какой из них использовать, зависит от вас
Хранилище с плавающей запятой по стандарту IEEE754
На самом деле, следующий контент взят из Вики
64 бита разделены, как показано на рисунке
Бит 0: флаговый бит символа Биты 1-11: биты экспоненты Биты 12-63: мантисса
к0.1Например,0.1Двоичный файл0.00011001100110011001100110011001100110011001100110011001100...
Тогда, во-первых, число положительное, а знак флагового бита = 0
Во-вторых, преобразуйте десятичную дробь в экспоненту, показатель степени равен -4, то есть показатель степени = 2 ^ 10 - 4 = 1019.
1.1001100110011001100110011001100110011001100110011001100... * 2^-4
Из-за научной нотации первое число всегда равно 1, поэтому память можно игнорировать, пока хранятся следующие 52 бита.
Если оно превышает 52 бита, 53-й бит округляется до 0, и результат равен100110011001100110011001100110011001100110011001101.
Хранилище двойной точности с плавающей запятой, вероятно, таково, что также отвечает на приведенные выше сомнения.
Это все для этой статьи.
Не по теме: я люблю читать, но я не прошу глубокого понимания; когда у меня есть ключ, я с радостью забуду поесть
Ссылаться на
[1] Ловушки с плавающей запятой в JavaScript и решения
Если вы считаете, что статья полезна для вас, то обратите внимание на паблик-аккаунт [сообщество IVWEB]~