js 数字计算的精度问题

〇、js 的数值计算存在结果不精确的情况

最近接触财务相关系统,页面上会有一些简单的计算,就发现其实是非常简单的计算,但 js 计算出来的结果却不是预期值,可能带上一大串 0 或 9,导致计算结果错误,本文来简单汇总下,以及如何处理来避免这个问题。

先看看都会有哪些不精确的情况。

// 【加减乘除 四种基础运算】
// 精度问题其实也不是全都会有,比如以下测试:
let num1 = 0.1;
let num2 = 0.1;
let result = num1 + num2;
console.log("0.1+0.1 = " + result);
// 0.1+0.1 = 0.2
// 0.1+0.2 = 0.30000000000000004
// 1.1*1.2 = 1.32
// 1.1*1.1 = 1.2100000000000002
// 其实四种基础运算都会出现类似情况,这里就简单列两类

// 【数值比较】
console.log("'0.1+0.2 = 0.3' 比较结果:", 0.1 + 0.2==0.3);
// '0.1+0.2 = 0.3' 比较结果: false

一、原因简介

因为计算机内部的信息都是由二进制方式表示的,即 0 和 1 组成的各种编码,但由于某些浮点数没办法用二进制准确的表示出来,也就带来了一系列精度问题。当然这也不是 js 独有的问题。

推荐一个文章吧,有兴趣可以深究下:https://zhuanlan.zhihu.com/p/33333351,讲的应该是很详细,但有些复杂博主看了一遍也不是太了解。

下面把大概的流程梳理下。

  • 将小数转成二进制数

小数部分乘 2 取整数部分,若小数不为 0 则继续乘 2,直至小数部分为 0。然后将取出的整数位正序排列。整数部分就除以 2。

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
......

0.1 的二进制表示是:0.000110011......0011.....(0011无限循环)

0.2 的二进制表示是:0.00110011......0011......(0011无限循环)

虽然是无限长度,但是还是得有一个标准的显示方式,那就是 IEEE 754 标准。

  • IEEE 754 标准方法表示数值

IEEE 754 标准是 IEEE 二进位浮点数算术标准(IEEE Standard for Floating-Point Arithmetic)的标准编号。IEEE 754 标准规定了计算机程序设计环境中的二进制和十进制的浮点数自述的交换、算术格式以及方法。

详细的标识法,就跳过了,直接来看结果。

js 只有一种数字类型 number,而 number 使用的是 IEEE 754 双精度浮点格式。最高位是一位符号位(0正 1负),后面的 11 位是指数,剩下的 52 位为尾数(有效数字)。

// js 中 0.1 的二进制存储格式为:
// 符号位用逗号分隔,指数位用分号分隔
0.1:0,01111111011;1001100110011001100110011001100110011001100110011010
0.2:0,01111111100;1001100110011001100110011001100110011001100110011010
  • 计算两个二进制数的和

    // 二进制形式的结果
    sum = 0.010011001100110011001100110011001100110011001100110100
    // 最后再转成十进制
    sum = 2^2 + 2^5 + 2^6 + ... + 2^52 = 0.30000000000000004440892098500626
    // 近似后的结果:0.30000000000000004

详情可参考:https://zhuanlan.zhihu.com/p/33333351

二、解决方式

2.1 Math.Round() 函数,先乘后除 10 的 n 次方

根据需求的精度,先乘以 10 的 n 次方,通过 Math.Round() 函数取整 后,再除以10 的 n 次方。

function numToString(num){
    let factor = Math.pow(10, 4); // 最多保留 4 位小数
    // 通过乘以一个因子(例如 10 的 4 次方),然后四舍五入
    // 最后,再除以相同的因子,可以实现对特定小数位数的精确控制
    let roundedNum = Math.round(num * factor) / factor;
    return roundedNum.toString();
}

关于 Math.Round() 四舍五入的规则,可以参考以下测试结果:

console.log("Math.round(4.2)     ", Math.round(4.2)  );
console.log("Math.round(4.5)     ", Math.round(4.5)  );
console.log("Math.round(4.7)     ", Math.round(4.7)  );
console.log("Math.round(-4.2)    ", Math.round(-4.2) );
console.log("Math.round(-4.5)    ", Math.round(-4.5) );
console.log("Math.round(-4.7)    ", Math.round(-4.7) );
console.log("Math.round(1.5)     ", Math.round(1.5)  );
console.log("Math.round(2.5)     ", Math.round(2.5)  );
console.log("Math.round(3.5)     ", Math.round(3.5)  );
console.log("Math.round(-1.5)    ", Math.round(-1.5) );
console.log("Math.round(-2.5)    ", Math.round(-2.5) );
console.log("Math.round(-3.5)    ", Math.round(-3.5) );

可以看出,正数的小数位为 5 时,进 1;负数小数位为 5 时,舍弃

2.2 toFixed() 方法,直接取小数点后固定位数

此方法就是直接指定要保留的几位小数,若小数位较少,就会以 0 补全

toFixed() 的四舍五入规则,并非严格的根据要保留小数位后边的小数来判断。

若作为判断的小数位为 5,且后边没有大于 0 的数,则舍入到最近的奇数;若 5 后边有非零的值,就直接进 1。

例如,4.55 保留 1 位小数,就是 4.5,但 4.5500001 的结果就是 4.6。

例如,-4.55 保留 1 位小数,就是 -4.5,但 -4.5500001 的结果就是 -4.6。

如下示例,供参考:

let num = 4.22;
console.log("num.toFixed(1):4.22)     ", num.toFixed(1));
num = 4.55;
console.log("num.toFixed(1):4.55)     ", num.toFixed(1));
num = 4.551;
console.log("num.toFixed(1):4.551)    ", num.toFixed(1));
num = 4.65;
console.log("num.toFixed(1):4.65)     ", num.toFixed(1));
num = 4.77;
console.log("num.toFixed(1):4.77)     ", num.toFixed(1));
num = -4.22;
console.log("num.toFixed(1):-4.22)    ", num.toFixed(1));
num = -4.55;
console.log("num.toFixed(1):-4.55)    ", num.toFixed(1));
num = -4.551;
console.log("num.toFixed(1):-4.551)   ", num.toFixed(1));
num = -4.65;
console.log("num.toFixed(1):-4.65)    ", num.toFixed(1));
num = -4.77;
console.log("num.toFixed(1):-4.77)    ", num.toFixed(1));
num = -4.77;
console.log("num.toFixed(1):-4.77)    ", num.toFixed(4));

注意:toFixed() 的结果是字符串类型,若最终还需要 number 类型,就需要通过 Number() 函数进行转换。

2.3 通过正则表达式,多余位小数直接舍去

必须先将数字类型转换成字符串,再使用 match() 方法。

let num = 3.14959;
let numStr = num.toString(); // 必须为字符串
let fixedNumStr = numStr.match(/^-?\d+(\.\d{0,2})?/)[0]; // 两位小数的正则表达式
let fixedNum = parseFloat(fixedNumStr);
console.log(fixedNum); // 输出:3.14
相关推荐
T^T尚3 小时前
uniapp H5上传图片前压缩
前端·javascript·uni-app
出逃日志4 小时前
JS的DOM操作和事件监听综合练习 (具备三种功能的轮播图案例)
开发语言·前端·javascript
XIE3924 小时前
如何开发一个脚手架
前端·javascript·git·npm·node.js·github
山猪打不过家猪4 小时前
React(五)——useContecxt/Reducer/useCallback/useRef/React.memo/useMemo
前端·javascript·react.js
前端青山4 小时前
React事件处理机制详解
开发语言·前端·javascript·react.js
科技D人生4 小时前
Vue.js 学习总结(14)—— Vue3 为什么推荐使用 ref 而不是 reactive
前端·vue.js·vue ref·vue ref 响应式·vue reactive
对卦卦上心4 小时前
React-useEffect的使用
前端·javascript·react.js
练习两年半的工程师4 小时前
React的基本知识:事件监听器、Props和State的区分、改变state的方法、使用回调函数改变state、使用三元运算符改变state
前端·javascript·react.js
啵咿傲4 小时前
在React中实践一些软件设计思想 ✅
前端·react.js·前端框架
GIS好难学5 小时前
《Vue零基础入门教程》第二课:搭建开发环境
前端·javascript·vue.js·ecmascript·gis·web