Кризис сложения и вычитания в JavaScript — почему это происходит?

JavaScript

каталог

Чем отличается передок без закидывания от соленой рыбы?

содержание
каталог
2 Предисловие
Три проблемы повторяются
3.1 Происхождение: Стандарт IEEE 754
3.2 Воспроизведение: вычислительный процесс
3.3 Расширение: цифровая безопасность
Решение четырех проблем
4.1 toFixed()
4.2 Рукописное простое сложение, вычитание, умножение и деление
Пять готовых фреймворков
6 ссылок

2 Предисловие

Назад к содержанию

В ежедневных рабочих расчетах мы идем по тонкому льду, но JavaScript всегда может преподнести нам такие сюрпризы~

  1. 0,1 + 0,2 = ?
  2. 1-- 0,9 =?

Если маленький партнер дает внутренний результат:

  1. 0.1 + 0.2 = 0.3
  2. 1 - 0.9 = 0.1

Тогда маленький друг получит оплеуху от того, что:

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(1 - 0.9); // 0.09999999999999998

Почему это происходит? Давайте узнаем!

Три проблемы повторяются

Назад к содержанию

Ниже мы воспроизведем проблему, обсуждая стандарт IEEE 754 и процесс вычисления сложения и вычитания JavaScript.

3.1 Происхождение: Стандарт IEEE 754

Назад к содержанию

Использование чисел в JavaScriptIEEE 754Стандартное 64-битное число двойной точности с плавающей запятой. Эта спецификация определяет формат чисел с плавающей запятой.Для 64-битных чисел с плавающей запятой, представленных в памяти, старший 1 бит является знаком, следующие 11 битов являются показателем степени, а оставшиеся 52 бита являются значащими цифрами, в частности :

  • Бит 0: Знаковый бит. использоватьsУказывает, что 0 — положительное число, а 1 — отрицательное;
  • Биты 1–11: Сохраняет часть экспоненты. использоватьeВыражать;
  • 12 - 63: дробная часть хранения (то есть значительные фигуры). использоватьfВыражать.

Знаковый бит определяет положительное или отрицательное число, экспоненциальная часть определяет размер значения, а дробная часть определяет точность значения.

IEEE 754 предусматривает, что первая цифра значащей цифры по умолчанию всегда равна 1 и не сохраняется в 64-битном числе с плавающей запятой.

То есть эффективное число всегда1.XX......XXXX......XXхранится в 64-битных числах с плавающей запятой, длина которых может достигать 52 бит.

Таким образом, JavaScript предоставляет мантиссу до 53 двоичных цифр (последние 52 бита 64-битного числа с плавающей запятой + 1 для первой мантиссы).

3.2 Воспроизведение: вычислительный процесс

Назад к содержанию

Что происходит при вычислении 0,1 + 0,2 с помощью JavaScript?

1. Измените 0.1 и 0.2 на двоичное представление:

0.1 -> 0.0001100110011001...(无限)
0.2 -> 0.0011001100110011...(无限)

Числа с плавающей запятой бесконечны в двоичных выражениях

2. Поскольку дробная часть 64-битного числа двойной точности с плавающей запятой стандарта IEEE 754 поддерживает до 53 двоичных разрядов, двоичный код, полученный после сложения двух, будет следующим:

0.0100110011001100110011001100110011001100110011001100

Из-за ограничения десятичных разрядов чисел с плавающей запятой это двоичное число усекается, и при преобразовании этого двоичного числа в десятичное оно становится равным 0,3000000000000000004, что вызывает ошибки в арифметических вычислениях.

3.3 Расширение: цифровая безопасность

Назад к содержанию

После прочтения расчета десятичного времени не точен.jsliangЯ чувствую, что необходимо снова поговорить о целых числах, потому что у целых чисел тоже есть некоторые проблемы:

console.log(19571992547450991);
// 19571992547450990

console.log(19571992547450991 === 19571992547450994);
// true

Разве это не удивительно!

потому что в JavaScriptNumberТипы единообразно обрабатываются как числа с плавающей запятой, и целые числа не могут избежать этой проблемы:

// 最大值
const MaxNumber = Math.pow(2, 53) - 1;
console.log(MaxNumber); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991

// 最小值
const MinNumber = -(Math.pow(2, 53) - 1);
console.log(MinNumber); // -9007199254740991
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991

То есть безопасный диапазон целых чисел:[-9007199254740991, 9007199254740991].

Если он превышает этот диапазон, возникает проблема точности, которая отбрасывается.

Конечно, эта проблема не только существует в JavaScript, практически все языки программирования с стандартами IEEE-745, только на многих других языках было инкапсулировано на многих других языках, чтобы избежать проблем точности.

А поскольку JavaScript — язык со слабой типизацией, в задумке проекта не существует строгого типа данных для чисел с плавающей запятой, поэтому проблема ошибки точности особенно актуальна.

На данный момент мы видим, что у JavaScript могут быть проблемы при работе с числовыми типами.

На самом деле могут быть проблемы на работе!

Однажды я занимался расчетом рабочего листа, а потом на следующий день мне сказали, что проблема с онлайном, а потом меня спросила продакт-леди:

  • Почему ваш компьютер не может выполнять десятичные вычисления, которые могут делать учащиеся начальной школы?

После трех секунд тишины найдите и изучите приведенное выше и, наконец, найдите решение ниже.

Решение четырех проблем

Назад к содержанию

Следующие попытки решить проблему вычисления с плавающей запятой различными способами.

4.1 toFixed()

Назад к содержанию

toFixed()Метод использует представление с фиксированной точкой для форматирования значения.

грамматика:numObj.toFixed(digits)

параметр:digits. Количество цифр после запятой; от 0 до 20 (включительно), среда реализации может поддерживать больший диапазон. Если этот параметр опущен, по умолчанию он равен 0.

const num = 12345.6789;

num.toFixed(); // '12346':进行四舍五入,不包括小数部分。
num.toFixed(1); // '12345.7':进行四舍五入,保留小数点后 1 个数字。
num.toFixed(6); // '12345.678900':保留小数点后 6 个数字,长度不足时用 0 填充。
(1.23e+20).toFixed(2); // 123000000000000000000.00 科学计数法变成正常数字类型

toFixed()РезультатStringвведите, не забудьте преобразоватьNumberТипы.

toFixed()метод использует запись с фиксированной точкой для форматирования числа, округляя результат.

пройти черезtoFixed()Мы можем решить некоторые проблемы:

Исходный множитель сложения и вычитания:

console.log(1.0 - 0.9);
// 0.09999999999999998

console.log(0.3 / 0.1);
// 2.9999999999999996

console.log(9.7 * 100);
// 969.9999999999999

console.log(2.22 + 0.1);
// 2.3200000000000003

использоватьtoFixed():

// 公式:parseFloat((数学表达式).toFixed(digits));
// toFixed() 精度参数须在 0 与20 之间

parseFloat((1.0 - 0.9).toFixed(10));
// 0.1   

parseFloat((0.3 / 0.1).toFixed(10));
// 3  

parseFloat((9.7 * 100).toFixed(10));
// 970

parseFloat((2.22 + 0.1).toFixed(10));
// 2.32

Итак, вот наступает вопрос:

  • parseFloat(1.005.toFixed(2))

что ты получишь, твоя реакция не1.01?

Однако нет, результат таков:1.

Как говорится, энм... пади! о (╥﹏╥) о

toFixed()Это также оказалось не самым безопасным решением.

4.2 Рукописное простое сложение, вычитание, умножение и деление

Назад к содержанию

Поскольку собственные методы JavaScript не могут спасти себя, мы можем только изменить свое мышление:

  • Преобразование дробной части JavaScript в строку для вычисления
/**
 * @name 检测数据是否超限
 * @param {Number} number 
 */
const checkSafeNumber = (number) => {
  if (number > Number.MAX_SAFE_INTEGER || number < Number.MIN_SAFE_INTEGER) {
    console.log(`数字 ${number} 超限,请注意风险!`);
  }
};

/**
 * @name 修正数据
 * @param {Number} number 需要修正的数字
 * @param {Number} precision 端正的位数
 */
const revise = (number, precision = 12) => {
  return +parseFloat(number.toPrecision(precision));
}

/**
 * @name 获取小数点后面的长度
 * @param {Number} 需要转换的数字
 */
const digitLength = (number) => {
  return (number.toString().split('.')[1] || '').length;
};

/**
 * @name 将数字的小数点去掉
 * @param {Number} 需要转换的数字
 */
const floatToInt = (number) => {
  return Number(number.toString().replace('.', ''));
};

/**
 * @name 精度计算乘法
 * @param {Number} arg1 乘数 1
 * @param {Number} arg2 乘数 2
 */
const multiplication = (arg1, arg2) => {
  const baseNum = digitLength(arg1) + digitLength(arg2);
  const result = floatToInt(arg1) * floatToInt(arg2);
  checkSafeNumber(result);
  return result / Math.pow(10, baseNum);
  // 整数安全范围内的两个整数进行除法是没问题的
  // 如果有,证明给我看
};

console.log('------\n乘法:');
console.log(9.7 * 100); // 969.9999999999999
console.log(multiplication(9.7, 100)); // 970

console.log(0.01 * 0.07); // 0.0007000000000000001
console.log(multiplication(0.01, 0.07)); // 0.0007

console.log(1207.41 * 100); // 120741.00000000001
console.log(multiplication(1207.41, 100)); // 0.0007

/**
 * @name 精度计算加法
 * @description JavaScript 的加法结果存在误差,两个浮点数 0.1 + 0.2 !== 0.3,使用这方法能去除误差。
 * @param {Number} arg1 加数 1
 * @param {Number} arg2 加数 2
 * @return arg1 + arg2
 */
const add = (arg1, arg2) => {
  const baseNum = Math.pow(10, Math.max(digitLength(arg1), digitLength(arg2)));
  return (multiplication(arg1, baseNum) + multiplication(arg2, baseNum)) / baseNum;
}

console.log('------\n加法:');
console.log(1.001 + 0.003); // 1.0039999999999998
console.log(add(1.001, 0.003)); // 1.004

console.log(3.001 + 0.07); // 3.0709999999999997
console.log(add(3.001, 0.07)); // 3.071

/**
 * @name 精度计算减法
 * @param {Number} arg1 减数 1
 * @param {Number} arg2 减数 2
 */
const subtraction = (arg1, arg2) => {
  const baseNum = Math.pow(10, Math.max(digitLength(arg1), digitLength(arg2)));
  return (multiplication(arg1, baseNum) - multiplication(arg2, baseNum)) / baseNum;
};

console.log('------\n减法:');
console.log(0.3 - 0.1); // 0.19999999999999998
console.log(subtraction(0.3, 0.1)); // 0.2

/**
 * @name 精度计算除法
 * @param {Number} arg1 除数 1
 * @param {Number} arg2 除数 2
 */
const division = (arg1, arg2) => {
  const baseNum = Math.pow(10, Math.max(digitLength(arg1), digitLength(arg2)));
  return multiplication(arg1, baseNum) / multiplication(arg2, baseNum);
};

console.log('------\n除法:');
console.log(0.3 / 0.1); // 2.9999999999999996
console.log(division(0.3, 0.1)); // 3

console.log(1.21 / 1.1); // 1.0999999999999999
console.log(division(1.21, 1.1)); // 1.1

console.log(1.02 / 1.1); // 0.9272727272727272
console.log(division(1.02, 1.1)); // 数字 9272727272727272 超限,请注意风险!0.9272727272727272

console.log(1207.41 / 100); // 12.074100000000001
console.log(division(1207.41, 100)); // 12.0741

/**
 * @name 按指定位数四舍五入
 * @param {Number} number 需要取舍的数字
 * @param {Number} ratio 精确到多少位小数
 */
const round = (number, ratio) => {
  const baseNum = Math.pow(10, ratio);
  return division(Math.round(multiplication(number, baseNum)), baseNum);
  // Math.round() 进行小数点后一位四舍五入是否有问题,如果有,请证明出来
  // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/round
}

console.log('------\n四舍五入:');
console.log(0.105.toFixed(2)); // '0.10'
console.log(round(0.105, 2)); // 0.11

console.log(1.335.toFixed(2)); // '1.33'
console.log(round(1.335, 2)); // 1.34

console.log(-round(2.5, 0)); // -3
console.log(-round(20.51, 0)); // -21

В этом коде мы сначала вычисляем с помощью умножения каменным молотком и преобразуем числа в целые числа для вычисления, в результате чего получаетсяБезопасностьДанные.

Есть ли проблема с JavaScript Integer Arithmetic?

После вычисления умножения предположим, что умножение не представляет проблемы, а затем выведите три операции сложения, вычитания и деления через умножение.

Наконец, правила округления устанавливаются посредством умножения и деления.

JavaScript Math.round()Будут ли сгенерированные числа проблемой?

Таким образом, мы выполнили сложение, вычитание, умножение и деление и округление двух чисел (сохранив заданную длину), так что не возникнет ли при этом проблем?

Если да, приведите пример.

Если нет, можете ли вы реализовать сложение, вычитание, умножение и деление трех чисел или даже нескольких чисел на основе сложения, вычитания, умножения и деления двух вышеуказанных чисел?

Пять готовых фреймворков

Назад к содержанию

Такой важный расчет, если ты напишешь его сам, всегда будешь чувствовать себя неловко и переполнен кризисом.

Так много раз мы можем использовать вычислительную библиотеку JavaScript, написанную большими парнями, потому что большие ребята уже сделали для нас множество тестов, что значительно снижает проблемы с нашим почерком, поэтому мы можем вызывать классы, написанные другими. библиотека.

Вот несколько хороших библиотек, которые можно порекомендовать:

Math.js — это дополнительная математическая библиотека для JavaScript и Node.js.

Он имеет гибкий анализатор выражений, поддерживающий символьную оценку, большое количество встроенных функций и констант, а также предоставляет интегрированное решение для обработки различных типов данных, таких как числа, большие числа, комплексные числа, дроби, единицы измерения и матрицы.

Мощный и простой в использовании.

Десятичный тип произвольной точности для JavaScript.

Небольшая, быстрая и простая в использовании библиотека для десятичной арифметики произвольной точности.

Библиотека JavaScript для арифметики произвольной точности.

И последнее, но не менее важное, стоит упомянуть: если вы очень строги с числами, возможно, вы можете скинуть параметры на бэкэнд, пусть бэкэнд сделает расчеты и вернет вам результат.

Например, это включает в себя расчет биткойнов, цен на товары в торговых центрах и т. д. ~

6 ссылок

Назад к содержанию

Отдайте дань уважения большим людям, которые внесли свой вклад в область вычислений JavaScript. В этой статье в основном используется содержание следующих статей, и в ней сделаны личные попытки и интеграции. Спасибо большим людям за их литературу:

Если вы хотите узнать больше, пожалуйста, следуйтеjsliangБиблиотека документации для:document-library


知识共享许可协议
библиотека документации jsliangЗависит отЛян ЦзюньронгиспользоватьCreative Commons Attribution-NonCommercial-ShareAlike 4.0 Международная лицензияЛицензия.
на основеGitHub.com/l ian Jun Ron…Создание работ выше.
Права на использование, отличные от разрешенных в настоящем Лицензионном соглашении, могут быть получены отCreative Commons.org/licenses/не…получено в.