解决JS小数计算精度问题
在JavaScript中,整数和浮点数都属于Number类型,使用64位存储一个数字(IEEE 754 双精度浮点数标准,所有语言遵循此标准的浮点数运算都会存在类似问题)。
虽然它们存储数据的方式是一致的,但是在进行数值运算时,却会表现出明显的差异性。整数参与运算时,得到的结果往往会和我们所想的一样。而对于浮点型运算,有时却会出现一些意想不到的结果。
// 加法
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
// 减法
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998
// 乘法
0.7 * 180 = 125.99999999999999
9.7 * 100 = 969.9999999999999
// 除法
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999
原因:
IEEE 754 双精度浮点数标准:一个浮点型数在计算机中的表示,它总共长度是64位,其中最高位为符号位(正负),接下来的11位为指数位(范围控制),最后的52位为小数位/尾数位(有效数字)。

因为浮点型数使用64位存储时,最多只能存储52位的小数位,对于一些存在无限循环的小数位浮点数,会截取前52位,从而丢失精度。
以 0.1 + 0.2 = 0.30000000000000004 的运算为例
- 首先将各个浮点数的小数位按照“乘2取整,顺序排列”的方法转换成二进制表示。具体做法是用2乘以十进制小数,得到积,将积的整数部分取出;然后再用2乘以余下的小数部分,又得到一个积;再将积的整数部分取出,如此推进,直到积中的小数部分为零为止。然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位,得到最终结果。
- 因此 0.1 转换成二进制表示为 0.0 0011 0011 0011 0011 0011 0011……(无限循环)
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.4 * 2 = 0.8 //取出整数部分0
0.8 * 2 = 1.6 //取出整数部分1
0.6 * 2 = 1.2 //取出整数部分1
- 同理对 0.2 进行二进制的转换,计算过程与上面类似,直接从0.2开始,相比于0.1,少了第一位的0,其余位数完全相同,结果为为 0.0011 0011 0011 0011 0011 0011……(无限循环)
- 将 0.1 与 0.2 相加,然后转换成 52 位精度的浮点型表示。得到的结果为 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100,转换成十进制值为 0.30000000000000004。
0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (0.1)
+ 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 (0.2)
= 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100
解决思路:
主要思路是将浮点数先乘以一定的数值转换为整数,通过整数进行运算,然后将结果除以相同的数值转换成浮点数后返回。

代码:
- TS版本
type Args = Array<number | number[]>;
/**
* 数学运算工具对象 (TypeScript 版)
* 提供精确的加减乘除和取模运算,解决浮点数精度问题
* 主要思路是将浮点数先乘以一定的数值转换为整数,通过整数进行运算,然后将结果除以相同的数值转换成浮点数后返回
*/
export const BCMath = {
/**
* 处理传入参数,统一转换为数组格式
* @param args 可以是数字、数字数组
* @returns 扁平化后的数字数组
*/
getParam(args: Args): number[] {
return Array.prototype.concat.apply([], args);
},
/**
* 计算单个数字的乘数因子
* 1.首先判断是否有小数点,如果没有,则返回1;
* 2.有小数点时,将小数位数的长度作为Math.pow()函数的参数进行计算
* 例如2的乘数因子为1,2.01的乘数因子为100
* @param x 需要计算的数字
* @returns 乘数因子 (10 的小数位数次方)
*/
multiplier(x: number) {
let parts = x.toString().split(".");
return parts.length < 2 ? 1 : Math.pow(10, parts[1].length);
},
/**
* 计算多个数字的最大乘数因子
* 例如1.3的乘数因子为10,2.13的乘数因子为100,则1.3和2.13的最大乘数因子为100
* @param numbers 需要计算的数字数组
* @returns 最大的乘数因子
*/
correctionFactor(argArr: number[]) {
return argArr.reduce((accum, next) => {
let num = this.multiplier(next);
return Math.max(accum, num);
}, 1);
},
/**
* 精确加法运算
* @param args 多个数字或数字数组
* @returns 精确相加后的结果
*/
add(...args: Args) {
const calArr = this.getParam(args);
const corrFactor = this.correctionFactor(calArr);
const sum = calArr.reduce((accum, curr) => {
return accum + Math.round(curr * corrFactor);
}, 0);
return sum / corrFactor;
},
/**
* 精确减法运算
* @param args 多个数字或数字数组
* @returns 精确相减后的结果
*/
sub(...args: Args) {
const calArr = this.getParam(args);
const corrFactor = this.correctionFactor(calArr);
const difference = calArr.reduce((accum, curr, curIndex) => {
// reduce()函数在未传入初始值时,accum初始化为数组中的第一个值,curr初始化为数组中的第二个值,curIndex从1开始
if (curIndex === 1) {
return Math.round(accum * corrFactor) - Math.round(curr * corrFactor);
}
// accum作为上一次运算的结果,就无须再乘以最大因子
return Math.round(accum) - Math.round(curr * corrFactor);
});
return difference / corrFactor;
},
/**
* 精确乘法运算
* @param args 多个数字或数字数组
* @returns 精确相乘后的结果
*/
mul(...args: Args) {
const calArr = this.getParam(args);
const corrFactor = this.correctionFactor(calArr);
const product = calArr
.map((item) => item * corrFactor)
.reduce((accum, curr) => {
return Math.round(accum) * Math.round(curr);
}, 1);
return product / Math.pow(corrFactor, calArr.length);
},
/**
* 精确除法运算
* @param args 多个数字或数字数组
* @returns 精确相除后的结果
*/
div(...args: Args) {
const calArr = this.getParam(args);
const quotient = calArr.reduce((accum, curr) => {
const corrFactor = this.correctionFactor([accum, curr]);
return Math.round(accum * corrFactor) / Math.round(curr * corrFactor);
});
return quotient;
},
/**
* 精确取模运算
* @param args 多个数字或数字数组
* @returns 精确取模后的结果
*/
mod(...args: Args) {
const calArr = this.getParam(args);
const remainder = calArr.reduce((accum, curr) => {
const corrFactor = this.correctionFactor([accum, curr]);
return (Math.round(accum * corrFactor) % Math.round(curr * corrFactor)) / corrFactor;
});
return remainder;
},
};
export default BCMath;
- JS版本
/**
* 数学运算工具对象 (TypeScript 版)
* 提供精确的加减乘除和取模运算,解决浮点数精度问题
* 主要思路是将浮点数先乘以一定的数值转换为整数,通过整数进行运算,然后将结果除以相同的数值转换成浮点数后返回
*/
export const BCMath = {
/**
* 处理传入参数,统一转换为数组格式
* @param args 可以是数字、数字数组
* @returns 扁平化后的数字数组
*/
getParam(args) {
return Array.prototype.concat.apply([], args);
},
/**
* 计算单个数字的乘数因子
* 1.首先判断是否有小数点,如果没有,则返回1;
* 2.有小数点时,将小数位数的长度作为Math.pow()函数的参数进行计算
* 例如2的乘数因子为1,2.01的乘数因子为100
* @param x 需要计算的数字
* @returns 乘数因子 (10 的小数位数次方)
*/
multiplier(x) {
let parts = x.toString().split(".");
return parts.length < 2 ? 1 : Math.pow(10, parts[1].length);
},
/**
* 计算多个数字的最大乘数因子
* 例如1.3的乘数因子为10,2.13的乘数因子为100,则1.3和2.13的最大乘数因子为100
* @param numbers 需要计算的数字数组
* @returns 最大的乘数因子
*/
correctionFactor(argArr) {
return argArr.reduce((accum, next) => {
let num = this.multiplier(next);
return Math.max(accum, num);
}, 1);
},
/**
* 精确加法运算
* @param args 多个数字或数字数组
* @returns 精确相加后的结果
*/
add(...args) {
const calArr = this.getParam(args);
const corrFactor = this.correctionFactor(calArr);
const sum = calArr.reduce((accum, curr) => {
return accum + Math.round(curr * corrFactor);
}, 0);
return sum / corrFactor;
},
/**
* 精确减法运算
* @param args 多个数字或数字数组
* @returns 精确相减后的结果
*/
sub(...args) {
const calArr = this.getParam(args);
const corrFactor = this.correctionFactor(calArr);
const difference = calArr.reduce((accum, curr, curIndex) => {
// reduce()函数在未传入初始值时,accum初始化为数组中的第一个值,curr初始化为数组中的第二个值,curIndex从1开始
if (curIndex === 1) {
return Math.round(accum * corrFactor) - Math.round(curr * corrFactor);
}
// accum作为上一次运算的结果,就无须再乘以最大因子
return Math.round(accum) - Math.round(curr * corrFactor);
});
return difference / corrFactor;
},
/**
* 精确乘法运算
* @param args 多个数字或数字数组
* @returns 精确相乘后的结果
*/
mul(...args) {
const calArr = this.getParam(args);
const corrFactor = this.correctionFactor(calArr);
const product = calArr
.map((item) => item * corrFactor)
.reduce((accum, curr) => {
return Math.round(accum) * Math.round(curr);
}, 1);
return product / Math.pow(corrFactor, calArr.length);
},
/**
* 精确除法运算
* @param args 多个数字或数字数组
* @returns 精确相除后的结果
*/
div(...args) {
const calArr = this.getParam(args);
const quotient = calArr.reduce((accum, curr) => {
const corrFactor = this.correctionFactor([accum, curr]);
return Math.round(accum * corrFactor) / Math.round(curr * corrFactor);
});
return quotient;
},
/**
* 精确取模运算
* @param args 多个数字或数字数组
* @returns 精确取模后的结果
*/
mod(...args) {
const calArr = this.getParam(args);
const remainder = calArr.reduce((accum, curr) => {
const corrFactor = this.correctionFactor([accum, curr]);
return (Math.round(accum * corrFactor) % Math.round(curr * corrFactor)) / corrFactor;
});
return remainder;
},
};
export default BCMath;
分情破爱始乱弃,流落天涯思别离。
如花似玉负情意,影如白昼暗自迷。
随风浮沉千叶落,行色匆匆鬓已稀。

浙公网安备 33010602011771号