Ловушки с плавающей запятой в JavaScript и решения

внешний интерфейс программист JavaScript модульный тест

JavaScript 浮点数陷阱及解法

Как мы все знаем, операции с числами с плавающей запятой в 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 бита — это мантисса, а лишняя часть автоматически округляется до нуля.

64 bit allocation

Фактическое число может быть рассчитано по следующей формуле:

$ V = (-1)^{S}\times M \times 2^{E} $

Обратите внимание, что приведенная выше формула следует спецификации экспоненциального представления, которое составляет 0M = 001. E — целое число без знака, поскольку длина составляет 11 бит, а диапазон значений — от 0 до 2047. Но показатель степени в научной записи может быть отрицательным, поэтому вычтите промежуточное число 1023, [0,1022] будет отрицательным, а [1024,2047] положительным. как в 4.5 индекс чего-либоE = 1025, мантисса М равна 001.

Окончательная формула становится:

$ V = (-1)^{S}\times (M+1) \times 2^{E-1023} $

так4.5Окончательное представление (M=001, E=1025):

4.5 allocation map

(изображение создано из этогоwoohoo.binary convert.com/convert_both…)

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

0.1 allocation map

Преобразуется в десятичную как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)Это лишь очень маленькая часть посередине, и тем более разреженная и неточная она с двух сторон.
fig1.jpg

В ранней системе заказов 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% полное тестовое покрытие, хорошая читабельность кода, вы также можете использовать его в своем приложении!

Ссылаться на

Конечно, этот пост написан для рекрутинга! ! !

Отдел обработки больших данных Alibaba искренне набирает осадных львов. Не медлите, вдруг пройдет.
Отправьте свое резюмеneosoyn@gmail.com