前端金额运算精度丢失问题及解决方案

news/2025/1/9 15:50:49 标签: 前端, javascript

前言

前端开发中难免会遇到价格和金额计算的需求,这类需求所要计算的数值大多数情况下是要求精确到小数点后的多少位。但是因为JS语言本身的缺陷,在处理浮点数的运算时会出现一些奇怪的问题,导致计算不精确。

本文尝试从现象入手,分析造成这一问题原因,并总结和整合一些通用的解决方案,以供大家参考。

现象回顾

下面的是JS进行数值运算过程中常见的问题,这个问题有个专业的名称叫精度丢失

在 JavaScript 中整数和浮点数都属于 Number 数据类型,所有的数字都是以 64 位浮点数形式存储,整数也是如此。所以我们在打印 1.00 这样的浮点数的结果是 1 而非 1.00 。在一些特殊的数值表示中,例如金额,这样看上去有点别扭,但是至少值是正确了。然而要命的是,当浮点数做数学运算的时候,你经常会发现一些问题,举几个例子:

javascript">// 加法
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
0.2 + 0.4 = 0.6000000000000001
2.22 + 0.1 = 2.3200000000000003

// 减法
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998

// 乘法
19.9 * 100 = 1989.9999999999998
19.9 * 10 * 10 = 1990
1306377.64 * 100 = 130637763.99999999
1306377.64 * 10 * 10 = 130637763.99999999
0.7 * 180 = 125.99999999999999
9.7 * 100 = 969.9999999999999
39.7 * 100 = 3970.0000000000005

// 除法
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999

问题分析

JavaScript 里的数字是采用 IEEE 754 标准的 64 位双精度浮点数。

该规范定义了浮点数的格式,对于 64 位的浮点数在内存中的表示,最高的 1 位是符号位,接着的 11 位是指数,剩下的 52 位为有效数字,具体对应如下:

  • 第 0 位:符号位, s 表示 ,0 表示正数,1 表示负数;

  • 第 1 位到第 11 位:储存指数部分, e 表示 ;

  • 第 12 位到第 63 位:储存小数部分(即有效数字),f 表示,

符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。

IEEE 754 规定,有效数字第一位默认总是 1,不保存在 64 位浮点数之中。也就是说,有效数字总是 1.xx…xx 的形式,其中 xx..xx 的部分保存在 64 位浮点数之中,最长可能为 52 位。

因此,JavaScript 提供的有效数字最长为 53 个二进制位(64 位浮点的后 52 位 + 有效数字第一位的 1)。

那么,JavaScript 在计算 0.1 + 0.2 时,到底发生了什么呢?

首先,十进制的 0.10.2 都会被转换成二进制,但由于浮点数用二进制表达时是无穷的,就成了下面的样子。

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

IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 53 位二进制位,所以两者相加之后得到的二进制就是下面的样子。

javascript">0.0100110011001100110011001100110011001100110011001100

最终,因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了 0.30000000000000004,所以在进行算术计算时就产生了误差。

解决方案

通常情况下对计算精度要求高的业务场景都应该交给后端去计算和存储,因为后端有成熟的方案和工具来解决这种计算问题。

这里是对网上常见的几个解决方案的汇总和整理,但是方案一和方案二都存在一定的局限性,我会在对应的方案里进行说明。通常我们前端做这类运算通常只用于表现层,所以这两个方案基本是够用的。

方案一

在应对确定精度浮点数运算时,可以把金额转换成整数进行运算,最后再将结果转换成真实金额。

javascript">// 定义金额(保留两位小数)
const amount1 = 0.10;
const amount2 = 0.20;

// 转整数运算,并还原成真实金额
const result = ((amount1 * 100) + (amount2 * 100)) / 100;

// 结果
console.log(result); // 0.3

// 直接运算
console.log(amount1 + amount2); // 0.30000000000000004

需要注意的是上面的例子只能处理最多两位小数的运算场景,如果小数位不确定这个方法是行不通的。

方案二

JavaScript 内置的 toFixed() 方法可以将数字转换成保留指定小数位的字符串。这个方法适用于简单的金额计算。但需要注意舍入误差,因为转换后是字符串,失去了浮点数的特性,最后的结果坑你存在微小的误差。

javascript">// 定义金额
const amount1 = 0.1;
const amount2 = 0.2;

// 加法运算
const result = (amount1 + amount2).toFixed(2); // 保留两位小数

// 注意这里运算的结果应该是:0.30000000000000004
console.log(result); // 输出 "0.30"

toFixed 它是一个四舍六入五成双的诡异的方法(也叫银行家算法)。"四舍六入五成双"含义:对于位数很多的近似数,当有效位数确定后,其后面多余的数字应该舍去,只保留有效数字最末一位,这种修约(舍入)规则是“四舍六入五成双”,也即“4舍6入5凑偶”这里“四”是指≤4 时舍去,"六"是指≥6时进上,"五"指的是根据5后面的数字来定,当5后有数时,舍5入1;当5后无有效数字时,需要分两种情况来讲:5前为奇数,舍5入1;5前为偶数,舍5不进(0是偶数)。

第三方库

现代前端发展至今,已经有很多成熟的类库来帮助我们解决此类问题,这类类库通常有很好的通用性和兼容性。

下面我将推荐几个人气较高的数字计算类库。

Math.js

Math.js 是专门为 JavaScript 和 Node.js 提供的一个广泛的数学库。它具有灵活的表达式解析器,支持符号计算,配有大量内置函数和常量,并提供集成解决方案来处理不同的数据类型像数字,大数字 (超出安全数的数字),复数,分数,单位和矩阵,功能强大,易于使用。

官网:mathjs.org/

GitHub:GitHub - josdejong/mathjs: An extensive math library for JavaScript and Node.js

decimal.js

为 JavaScript 提供十进制类型的任意精度数值。

官网:decimal.js 

GitHub:https://github.com/MikeMcl/decimal.js/

big.js

Big.js 是一个用于处理任意精度的大数运算的 JavaScript 库。它解决了 JavaScript 中处理大数运算时精度丢失的问题,提供了更高精度的计算能力。

Big.js 库的特点包括:

  • 任意精度:Big.js 允许您处理任意精度的数字,而不受 JavaScript 内置数字类型的限制。

  • 高精度计算:Big.js 提供了精确的加法、减法、乘法、除法和取余等运算,以及比较和舍入等功能。

  • 可配置的精度和舍入规则:您可以自定义 Big.js 运算的精度和舍入规则,以满足特定的需求。

  • 支持链式操作:您可以使用链式调用来执行多个运算,使代码更简洁易读。

  • 适用于浏览器和 Node.js:Big.js 可以在浏览器和 Node.js 环境中使用,兼容性良好。

Big.js 库非常适用于需要高精度计算的场景,如金融、密码学、科学计算和大数据处理等。它允许开发人员在 JavaScript 中进行准确的数字计算,避免了精度损失带来的问题。

官网:big.js API[5]

GitHub:GitHub - MikeMcl/big.js: A small, fast JavaScript library for arbitrary-precision decimal arithmetic.

总结

本文对 Javascript 中浮点数运算出现的精度丢失问题进行了还原,分析了问题产生的原因在于二进制本身。同时给出了三个网络上比较成熟的解决方案,其中第一和第二方案基本可以满足大部分开发场景,如果不能满足就使用类库。

最后,建议所有对运算精度要求极高的业务场景都放到后端去运算,切记。


http://www.niftyadmin.cn/n/5817687.html

相关文章

C# 中await和async的用法(一)

在 C# 中,await 关键字用于异步编程,配合 async 方法一起使用。await 允许你等待异步操作完成,而不会阻塞当前线程。简而言之,await 会暂停当前方法的执行,直到任务完成,然后继续执行。 1. await与async的关…

Clojure语言的数据库编程

Clojure语言的数据库编程 引言 在当今社会,数据的处理和管理已经成为一个不可或缺的部分。无论是互联网应用、企业系统还是移动应用,都需要与数据库进行频繁的交互。因此,选择一种合适的编程语言和相应的库来进行数据库编程显得尤为重要。C…

易支付二次元网站源码及部署教程

易支付二次元网站源码及部署教程 引言 在当今数字化时代,二次元文化逐渐成为年轻人生活中不可或缺的一部分。为了满足这一庞大用户群体的需求,搭建一个二次元主题网站显得尤为重要。本文将为您详细介绍易支付二次元网站源码的特点及其部署教程&#xf…

左神算法基础巩固--2

文章目录 稳定性选择排序冒泡排序插入排序归并排序快速排序堆排序 哈希表链表解题 稳定性 稳定性是指算法在排序过程中保持相等元素之间相对顺序的特性。具体来说,如果一个排序算法是稳定的,那么对于任意两个相等的元素,在排序前它们的相对顺…

leetcode 458. 可怜的小猪

题目:458. 可怜的小猪 - 力扣(LeetCode) 数学问题。 尝试次数 times minutesToTest / minutesToDie 每只猪可以承载的数据量 bit times 1 答案 ret 就是 bit ^ ret > buckets 时,ret 的最小值。 特殊的,注意…

57. Three.js案例-创建一个带有聚光灯和旋转立方体的3D场景

57. Three.js案例-创建一个带有聚光灯和旋转立方体的3D场景 实现效果 该案例实现了使用Three.js创建一个带有聚光灯和旋转立方体的3D场景。 知识点 WebGLRenderer(WebGL渲染器) THREE.WebGLRenderer 是 Three.js 中用于将场景渲染为 WebGL 内容的核…

10. C语言 函数详解

本章目录: 前言1. C 语言函数概述1.1 函数的定义与结构1.2 函数声明1.3 函数调用 2. 函数参数传递2.1 传值调用2.2 传引用调用(模拟)2.3 引用调用(C 特性) 3. 内部函数与外部函数3.1 内部函数3.2 外部函数3.3 示例:多个…

TensorRT-LLM中的MoE并行推理

2种并行方式: moe_tp_size:按照维度切分,每个GPU拥有所有Expert的一部分权重。 moe_ep_size: 按照Expert切分,每个GPU有用一部分Expert的所有权重。 二者可以搭配一起使用。 限制:二者的乘积,必须等于模…