JS前端数字计算精度丢失问题,请问各位是怎么解决的?

比如计算百分比,乘除法,js计算有小数点,精度会丢失,通过toFixed可以截取小数后几位,但是会四舍五入,而toPrecision则需要指定数字长度。现在要保留小数点后2位,请问怎么处理

let n = 123.123;
console.log(n.toPrecision(4)) // 123.1
let n = 123.126;
console.log(n.toFixed(2)) // 123.13

期望结果:
let n = 123.1266646635525563;
// 123.12

阅读 11.6k
14 个回答

所谓“没有完美的算法,只有合适的场景”,浮点数字(小数)就是个很好的例子。

我们知道,按照目前的规范,JS 无法直接完美的处理小数。但是实际上,我们往往也不需要它“完美”处理小数。比如,以记账为例,我们通常只关心小数点后两位,也就是“分”,再细的,我们关注它意义不大,投入产出比太低。那么我们就可以把所有数字 * 100,取整后保存,带来的误差非常低。

所以,讨论小数精度问题,要看具体的需求。只要满足需求,怎么保存都可以。

类似的方法,函数其实很多,比如说可以用Math.floor函数,将结果向下取整,最后再除以100。这样可以保留小数点后2位并且不进行四舍五入。
可以这样:

let n = 123.1266646635525563;
let result = Math.floor(n * 100) / 100;
console.log(result); // 123.12

或者用字符串的substring函数截取小数点后的位数也是可以的,相对刚刚纯数字的方法多了一个转换步骤,相对复杂一点点,但有时候用得上,将len n转换为s字符串,然后在里面找到小数点的位置,最后再输出。

let n = 123.1266646635525563;
let result = n.toString();
let decimalIndex = result.indexOf('.') + 1;
result = result.substring(0, decimalIndex + 2);
console.log(parseFloat(result)); // 123.12

可以考虑先乘以100再整数取整,最后除以100恢复到原数字

Math.floor(n * 100) / 100

使用decimal.js,或者bignumber.js

自定义计算函数:

function multiply(a, b) {
    let m = 0;
    const s1 = a.toString();
    const s2 = b.toString();
    try { m += s1.split(".")[1].length } catch (e) {}
    try { m += s2.split(".")[1].length } catch (e) {}
    return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
}

function divide(a, b) {
    let t1 = 0;
    let t2 = 0;
    try { t1 = a.toString().split(".")[1].length } catch (e) {}
    try { t2 = b.toString().split(".")[1].length } catch (e) {}
    const r1 = Number(a.toString().replace(".", ""));
    const r2 = Number(b.toString().replace(".", ""));
    return multiply((r1 / r2), Math.pow(10, t2 - t1));
}

function add(a, b) {
    let c = 0;
    let d = 0;
    try { c = a.toString().split(".")[1].length } catch (e) {}
    try { d = b.toString().split(".")[1].length } catch (e) {}
    const m = Math.pow(10, Math.max(c, d));
    return (multiply(a, m) + multiply(b, m)) / m;
}

function subtract(a, b) {
    let c = 0;
    let d = 0;
    try { c = a.toString().split(".")[1].length } catch (e) {}
    try { d = b.toString().split(".")[1].length } catch (e) {}
    const m = Math.pow(10, Math.max(c, d));
    const n = (c >= d) ? c : d;
    return ((multiply(a, m) - multiply(b, m)) / m).toFixed(n);
}

let n = 123.1266646635525563;
let result = multiply(n, 100);
result = Math.floor(result);
result = divide(result, 100);
console.log(result); // 123.12
新手上路,请多包涵
// 示例:使用 Bigint 进行计算
const num1 = BigInt(0.1 * 10);
const num2 = BigInt(0.2 * 10);

const result = (num1 + num2) / BigInt(10);
console.log(result.toString()); // 输出: 0.3

解决精度缺失问题,要么是第三方库,要么就是自己写方法计算,自定义方法中的加减乘除大部分是基于计算小数位个数,如3位,计算前先乘1000,计算完之后再除以1000就可以了

math.js

新手上路,请多包涵

我的经验是解决计算精度 直接用现成的库去处理 比如上面说的BigNumber decimal等等, 我个人更喜欢jsbi-calculator, 加减乘除写起来简单 也不用像其他几个库那样要设置 引入的体积也更小
而保留小数主要看是不是要四舍五入或者银行家舍入 比如自己写一个舍入的方法 或者用lodash的round等 看个人选择

个人用的比较多的方法,加减除

function preciseAdd(num1, num2) {
  let m = 0, s1 = num1.toString(), s2 = num2.toString();
  try { m += s1.split(".")[1].length } catch (e) { }
  try { m += s2.split(".")[1].length } catch (e) { }
  return Number(s1.replace(".", "")) + Number(s2.replace(".", "")) / Math.pow(10, m);
}

function preciseMultiply(num1, num2) {
  let m = 0, s1 = num1.toString(), s2 = num2.toString();
  try { m += s1.split(".")[1].length } catch (e) { }
  try { m += s2.split(".")[1].length } catch (e) { }
  return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
}

function preciseDivide(num1, num2) {
  let t1 = 0, t2 = 0, s1 = num1.toString(), s2 = num2.toString();
  try { t1 = s1.split(".")[1].length } catch (e) { }
  try { t2 = s2.split(".")[1].length } catch (e) { }
  let r1 = Number(s1.replace(".", ""));
  let r2 = Number(s2.replace(".", ""));
  return (r1 / r2) * Math.pow(10, t2 - t1);
}
新手上路,请多包涵
/**
 * 数字运算(主要用于小数点精度问题)
 * [see](https://juejin.im/post/6844904066418491406#heading-12)
 * @param {number} a 前面的值
 * @param {"+"|"-"|"*"|"/"} type 计算方式
 * @param {number} b 后面的值
 * @example
 * 可链式调用
 * const res = computeNumber(1.3, "-", 1.2).next("+", 1.5).next("*", 2.3).next("/", 0.2).result;
 */
export function computeNumber(a, type, b) {
  /**
   * 获取数字小数点的长度
   * @param {number} n 数字
   */
  function getDecimalLength(n) {
    const decimal = n.toString().split(".")[1];
    return decimal ? decimal.length : 0;
  }
  /**
   * 修正小数点
   * @description 防止出现 `33.33333*100000 = 3333332.9999999995` && `33.33*10 = 333.29999999999995` 这类情况做的处理
   * @param {number} n
   */
  const amend = (n, precision = 15) => parseFloat(Number(n).toPrecision(precision));
  const power = Math.pow(10, Math.max(getDecimalLength(a), getDecimalLength(b)));
  let result = 0;

  a = amend(a * power);
  b = amend(b * power);

  switch (type) {
    case "+":
      result = (a + b) / power;
      break;
    case "-":
      result = (a - b) / power;
      break;
    case "*":
      result = (a * b) / (power * power);
      break;
    case "/":
      result = a / b;
      break;
  }

  result = amend(result);

  return {
    /** 计算结果 */
    result,
    /**
     * 继续计算
     * @param {"+"|"-"|"*"|"/"} nextType 继续计算方式
     * @param {number} nextValue 继续计算的值
     */
    next(nextType, nextValue) {
      return computeNumber(result, nextType, nextValue);
    },
  };
}

let m = 123.1923
let a = n.toString()
console.log(a.indexOf('.')===-1?(+a).toFixed(2):a.slice(0,a.indexOf('.')+3))
不四舍五入只能借助字符串 比如截取

新手上路,请多包涵

消除js计算误差add、reduce是传入的加减数字,reduce是被加数(被减数),s是加减符号

formatNum(add, reduce, s, num) {
  let m = Math.pow(10, num); //num是10的次幂
  let res = s == '+' ? (add * m + reduce * m) / m : (add * m - reduce * m) / m;
  return Math.round(res * m) / m;
},
let n= 123.123
console.log(this.formatNum(n, 0, '-', 2)) // 123.12

我们项目组用的是decimal.js库去解决这个问题的,所以哪一个库最好用希望有大佬来解答一下

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏