Skip to content

数字精度丢失的问题

专题简介

  • 浮点数
  • 问题分析
  • 解决方案

为什么会出现精度丢失

js 中存在小数点精度丢失的问题是由于其使用的浮点数表示方式。JS 采用的是双精度浮点数表示法,也称为 IEEE 754 标准,它使用 64 位来表示一个数字,其中 52 位用于表示有效数字,而其他位用于表示符号、指数和特殊情况。

由于使用有限的位数来表示无限的小数,JS 无法准确地表示某些小数。其中一个典型的示例是 0.1,它在二进制中是一个无限循环的小数。当我们将 0.1 这样的小数转换为二进制进行存储时,存在近似表示的误差,因此在进行计算时会出现精度丢失的问题。

小数点精度丢失的问题是由于 js 使用的浮点数表示法的特性所致,需要在开发中注意,并考虑使用其他方法或工具来解决精度问题

场景复现

一个经典的知识点题

js
0.1 + 0.2 === 0.3 // false

为什么会 false 因为计算机存储的会存在精度丢失的问题

问题分析

计算机存储双精度浮点数需要先把十进制数转换为二进制的科学记数法的形式,然后计算机以自己的规则{符号位+(指数位+指数偏移量的二进制)+小数部分}存储二进制的科学记数法

因为存储时有位数限制(64 位),并且某些十进制的浮点数在转换为二进制数时会出现无限循环,会造成二进制的舍入操作(0 舍 1 入),当再转换为十进制时就造成了计算误差

解决方案

理论上用有限的空间来存储无限的小数是不可能保证精确的,但我们可以处理一下得到我们期望的结果

当你拿到 1.4000000000000001 这样的数据要展示时,建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示,如下:

js
parseFloat((1.4000000000000001).toPrecision(12)) === 1.4 // True

封装成方法就是:

js
function strip(num, precision = 12) {
  return +parseFloat(num.toPrecision(precision))
}

对于运算类操作,如 +-*/,就不能使用 toPrecision 了。正确的做法是把小数转成整数后再运算。以加法为例:

js
/**
 * 精确加法
 */
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
}

最后还可以使用第三方库,如 Math.js、BigDecimal.js