1. Введение
Как мы все знаем, JavaScript имеет только числовой тип Number, а Number использует 64-битное кодирование чисел с плавающей запятой двойной точности в спецификации IEEE754. Таким образом, возникает классическая проблема 0,1 + 0,2 === 0,30000000000000004.
Давайте выведем процесс вычисления 0,1 + 0,2 с отношением «знать почему» и «знать почему».
2. Базовое преобразование
Для начала нам нужно понять, как преобразовать десятичное число в двоичное, метод следующий:
Умножьте число после запятой на 2, возьмите целую часть результата (либо 1, либо 0), затем умножьте дробную часть на 2, затем возьмите целую часть результата... и так далее до дробной части часть 0 или бит Число достаточно, и все в порядке. Затем расположите взятые целые части по порядку
По вышеуказанной методике находим двоичное число 0,1, и получается, что двоичное число после преобразования 0,1 такое:
0,000110011001100110011 (0011 бесконечный цикл)...
Таким образом, потеря точности — это не языковая проблема, а неотъемлемый недостаток хранения чисел с плавающей запятой. Числа с плавающей запятой не могут точно представлять все значения в пределах их числового диапазона, но могут точно представлять только те значения, которые могут быть представлены экспоненциальной записью m * 2 ^ e. Например, научная запись 0,5 равна 2 ^ ( -1), которые могут быть точно сохранены, а 0,1 и 0,2 не могут быть точно сохранены.
Так как же хранить это двоичное число бесконечного цикла?Вы не можете просто взять длину усечения. В настоящее время отражается роль спецификации IEEE754.
3. Спецификация IEEE754
IEEE754 предоставляет определение представления чисел с плавающей запятой. Формат следующий:
(-1)^S * M * 2^E
Значение каждого символа следующее: S — бит знака, который определяет, является ли он положительным или отрицательным, когда он равен 0, это положительное число, а когда он равен 1, это отрицательное число. M относится к эффективному количеству цифр, больше 1 и меньше 2. E, является битом экспоненты.
Тогда 0,1 представлен спецификацией IEEE754 как:
(-1)^0 * 1.100110011(0011)… * 2^-4
Для хранения чисел с плавающей запятой в компьютере спецификация IEEE754 обеспечивает кодирование чисел с плавающей запятой одинарной точности и кодирование чисел с плавающей запятой двойной точности.
IEEE754 предусматривает, что для 32-битных чисел с плавающей запятой одинарной точности старший 1 бит — это бит знака S, следующие 8 бит — это показатель степени E, а оставшиеся 23 бита — это мантисида M.
Для 64-битного числа с плавающей запятой двойной точности старший 1 бит — это бит знака S, следующие 11 бит — показатель степени E, а оставшиеся 52 бита — мантиссы M.
количество цифр | Заказ | значащие цифры/мантисса | |
---|---|---|---|
число с плавающей запятой одинарной точности | 32 | 8 | 23 |
число двойной точности с плавающей запятой | 64 | 11 | 52 |
В качестве примера мы возьмем числа с плавающей запятой одинарной точности для анализа фактического метода хранения 0,15625.
Преобразование 0,15625 в двоичное число равно 0,00101, что равно 1,01 * 2^(-3) в экспоненциальном представлении, поэтому бит знака равен 0, что указывает на то, что число положительное. Обратите внимание, что следующие 8 бит непосредственно хранят не показатель степени -3, а порядок, который определяется следующим образом:
Порядок = Экспонента + Смещение
Для данных с одинарной точностью заданное смещение равно 127, а для данных с двойной точностью заданное смещение равно 1023. Таким образом, порядок 0,15625 равен 124, что равно 01111100 в 8-битном двоичном формате.
Еще раз обратите внимание, что при сохранении значащих цифр 1 перед десятичной запятой не будет сохранена (поскольку первая цифра двоичных значащих цифр должна быть 1, что опущено), поэтому здесь сохраняется 01, что меньше 23 цифры, а остальное заполняется 0.
Конечно, здесь нужно объяснить еще одну проблему: как обрезать бесконечный цикл значащих цифр, таких как 0,1. Режим округления по умолчанию в IEEE754:
Round to nearest, ties to even
То есть округлить до ближайшего и представимого значения, а когда есть два одинаково близких числа, взять четное значение.
4. Вернемся к задаче 0.1+0.2===0.30000000000000004
JavaScript хранит все значения типа Number в 64-битных числах с плавающей запятой двойной точности.Согласно спецификации IEEE754, двоичное число 0,1 сохраняет только 52 значащие цифры, то есть 1,100110011001100110011001100110011001100110011001101 * 2^(-4). Мы используем - для разделения бита знака, бита порядка и бита мантиссы, поэтому фактический битовый шаблон 0,1 равен 0 - 01111111011 - 1001100110011001100110011001100110011001100110011010.
Точно так же двоичное число составляет 1.1001100110011001100110011001100110011001100110011001100110011001101 0,2 * 2 ^ (- 3), Таким образом, 0,2-битный режим на самом деле сохраняется 0-01111111100 - 10011001100110011001100110011001100110011001100110011001100110011010.
0.1 и 0.2 на самом деле расширяются, и конец добавляется к концу, результаты следующие:
0.00011001100110011001100110011001100110011001100110011010
+0.00110011001100110011001100110011001100110011001100110100
------------------------------------------------------------
=0.01001100110011001100110011001100110011001100110011001110
Если зарезервировано только 52 значащих цифры, двоичное число результата (0,1 + 0,2) равно 1,001100110011001100110011001100110011001100110011010 * 2^(-2), Опустите последний 0 мантиссы, т.е. 1.00110011001100110011001100110011001100110011001101 * 2^(-2), Таким образом, битовый шаблон, когда (0,1 + 0,2) фактически сохраняется, равен 0 - 01111111101 - 0011001100110011001100110011001100110011001100110100.
Десятичное число результата (0,1 + 0,2) равно 0,3000000000000000004, и теперь вывод завершен.
Мы можем убедиться, что наш процесс вывода совместим с браузером в Chrome.
Инструменты для новичковОн также предоставляет богатую базовую функцию преобразования, которая позволяет нам проверять точность результатов.
(0.1).toString('2')
// "0.0001100110011001100110011001100110011001100110011001101"
(0.2).toString('2')
// "0.001100110011001100110011001100110011001100110011001101"
(0.1+0.2).toString('2')
// "0.0100110011001100110011001100110011001100110011001101"
(0.3).toString('2')
// "0.010011001100110011001100110011001100110011001100110011"
5. Решить проблему потери точности
5.1 Библиотека классов
В NPM есть много математических библиотек, поддерживающих JavaScript и Node.js, таких как math.js, decimal.js, D.js и т. д.
5.2 Нативные методы
Метод toFixed() округляет число до указанного количества знаков после запятой. Но это не значит, что метод надежен. Тест на хроме выглядит следующим образом:
1.35.toFixed(1) // 1.4 正确
1.335.toFixed(2) // 1.33 错误
1.3335.toFixed(3) // 1.333 错误
1.33335.toFixed(4) // 1.3334 正确
1.333335.toFixed(5) // 1.33333 错误
1.3333335.toFixed(6) // 1.333333 错误
Мы можем переписать toFix, чтобы решить эту проблему. Необходимость переноса определяется путем определения того, является ли последняя цифра больше или равной 5. Если требуется перенос, сначала умножьте десятичную дробь на кратное, чтобы получить целое число, прибавьте 1, а затем разделите на кратное, чтобы получить целое число. десятичной, так что нет необходимости проводить по одному суд. Ссылаться настатья.
5.3 ES6
ES6 добавляет крошечную константу к объекту Number — Number.EPSILON.
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"
Цель введения такой малой величины - установить диапазон ошибок для вычисления с плавающей запятой.Если ошибка может быть меньше, чем Number.EPSILON, мы можем считать результат достоверным.
Функция проверки ошибок (из «Введение в стандарт ES6» — Руан Ифэн)
function withinErrorMargin (left, right) {
return Math.abs(left - right) < Number.EPSILON
}
withinErrorMargin(0.1+0.2, 0.3)