网站首页 > 博客文章 正文
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
1. 无法绕过的 JavaScript 数字精确度
当浮点数精度不够时,如何在 JavaScript 中处理精确的十进制计算?尤其是在牵涉到处理钱、百分比或任何需要精确的计算时。
// 1.基本算术问题不准确
0.1 + 0.2; // 输出 0.30000000000000004
1.0 - 0.9; // 输出 0.09999999999999998
0.7 + 0.1; // 输出 0.7999999999999999
// 2.除法问题不准确
1 / 3; // 0.3333333333333333
1 / 6; // 0.16666666666666666
// 乘法问题不准确
0.3 / 0.1; // 2.9999999999999996
(1 / 98) * 98; // 0.9999999999999999
// 货币计算不准确
19.99 * 0.1; // 1.9989999999999999
以上计算并非 JavaScript 语言错误,而是计算机存储十进制数的基本限制,其影响几乎所有编程语言。计算机以二进制方式存储数字,虽然这对整数非常有效,但却会产生小数问题。
在十进制系统中,开发者可以将 1/10 表示为 0.1。但在二进制中,0.1 是一个循环分数,就像试图用十进制表示 1/3(0.333333……)一样。当以 JavaScript 的 64 位浮点格式(IEEE 754)存储时则必须被截断,从而导致微小的舍入误差。
虽然 BigInt 一定程度上可以解决这个问题(不处理小数),但也有自己的局限性:
// BigInt 仅适用于整数
BigInt(1) / BigInt(3);
// BigInts不能用于除法而直接报错
BigInt(0.1);
// 不能将小数转化为 BigInt
从这点来看,BigInt 虽然可以解决整数的精度问题,但却无法处理小数。实际上,其通常用于加密或非常大的整数计算。因此,一种常见的解决方法是将所有数乘以 100 以完全避免小数:
// 常见方法:乘以倍数去除小数
const dollars = 19.99 * 100;
// 1999 cents
const tax = 8.5 * 100;
// 850 basis points
// Still breaks down
console.log((dollars * tax) / 10000);
// 169.91499999999996 还是会有舍入错误
该方法看起来很简单,但如果计算复杂或操作繁多,就会失效。
2.Fraction.js 使用分数而非小数
强烈推荐开发者使用 Fraction.js 来解决精度问题,这是一个将数字作为分数而不是浮点小数处理的库,其认为 1/3 永远比 0.333333 更精确。
Fraction.js 支持使用 BigInt 表示分子和分母,确保在提高准确性的同时最大程度地降低性能开销。其设计针对精度进行了优化,使其成为其他数学工具,例如: Polynomial.js 和 Math.js 的强大替代品。
import Fraction from "fraction.js";
// 基础数学运算是准确的
new Fraction(0.1).add(0.2).toString(2); // "0.30"
new Fraction(1.0).sub(0.9).toString(2); // "0.10"
new Fraction(0.7).add(0.1).toString(1); // "0.8"
// 指定精度的除法运算
new Fraction(1, 3).toString(2); // "0.33"
new Fraction("1/6").toString(2); // "0.17"
// 货币计算也是准确的
const price = new Fraction("19.99");
const tax = new Fraction("0.085"); // 8.5% tax
const total = price.add(price.mul(tax));
total.toString(2); // "21.69"
// 对于分数则用需要实际的分数表示
new Fraction("0.333333").toFraction(); // "1/3"
new Fraction("1.4166666").toFraction(); // "17/12"
Fraction.js 不会尝试用二进制表示小数(因为舍入误差),而是将每个数字存储为整数的比率,例如:0.1 在内部变为 1/10。
const C_ZERO = BigInt(0);
const C_ONE = BigInt(1);
// 已经简化
const parse = function (p1, p2) {
let n = C_ZERO,d = C_ONE, s = C_ONE;
if (p1 % 1 === 0) {
n = BigInt(p1);
} else if (p1 > 0) {
let z = 1;
let A = 0,B = 1;
let C = 1,D = 1;
let N = 10000000;
if (p1 >= 1) {
z = 10 ** Math.floor(1 + Math.log10(p1));
p1 /= z;
}
// Using Farey Sequences
while (B <= N && D <= N) {
let M = (A + C) / (B + D);
if (p1 === M) {
if (B + D <= N) {
n = A + C;
d = B + D;
} else if (D > B) {
n = C;
d = D;
} else {
n = A;
d = B;
}
break;
} else {
if (p1 > M) {
A += C;
B += D;
} else {
C += A;
D += B;
}
if (B > N) {
n = C;
d = D;
} else {
n = A;
d = B;
}
}
}
n = BigInt(n) * BigInt(z);
d = BigInt(d);
}
}
P["s"] = s < C_ZERO ? -C_ONE : C_ONE;
P["n"] = n < C_ZERO ? -n : n;
P["d"] = d < C_ZERO ? -d : d;
};
function Fraction(a, b) {
parse(a, b);
return newFraction(P["s"] * P["n"], P["d"]);
}
从 Fraction.js 的构造函数来看,其内部使用了 Farey 序列的数学概念。在给定的正整数 n 下,所有分子和分母均不超过 n 的最简分数的有序集合。
因此,parse 函数的主要目的是将给定的数字 p1 转换为一个最简的分数形式,确保分子和分母都是大整数,并处理正负符号。函数可用于需要将实数转换为分数的应用场景,例如:数值计算、比例表示等。
下面是 Fraction.js 的 add 、mul 方法的具体实现:
function mul(a, b) {
parse(a, b);
return newFraction(
this["s"] * P["s"] * this["n"] * P["n"],
this["d"] * P["d"]
);
}
function add(a, b) {
parse(a, b);
return newFraction(
this["s"] * this["n"] * P["d"] + P["s"] * this["d"] * P["n"],
this["d"] * P["d"]
);
}
对于 add 来说,其内部依然采用的是整数的运算法则,而非采用小数:
- this["s"] * this["n"] * P["d"]:表示 a 的分数形式与 b 的分母相乘
- P["s"] * this["d"] * P["n"]:表示 b 的分数形式与 a 的分母相乘。
这个加法公式实际上是通过通分(将两个分数转化为相同的分母)来实现的。
3.Fraction.js 的诸多限制
Fraction.js 功能虽然强大,但并非万能,其仅适用于有理数,即可以表示为整数比率的数字,例如:如 3/4 或 22/7 等。这使得其非常适合货币计算或分数,但对于无理数(如 π 或 √2)则不太适用。
同时, Fraction.js 在内部使用 BigInt,因此可以获得比常规小数更高的精度(注意不是无限精度)。但是某些运算也有限制,例如:平方根给出的是近似值而不是精确值,并且不支持 sin 或 cos 等函数,当然也包括复数。
即使有诸多限制,Fraction.js 仍然非常有用。开发者可以使用它来提前计算精确值,处理没有舍入误差的货币计算,构建与分数一起使用的软件,或解决小数精度真正重要的问题。
参考资料
https://www.trevorlasn.com/blog/fraction-numbers-in-javascript
https://github.com/rawify/Fraction.js
https://zhuanlan.zhihu.com/p/28162086
https://larrylu.dev/why-01-02-03-a-deep-dive-into-ieee-754-and-floating-point-arithmetic
猜你喜欢
- 2025-07-10 Go 语言结构 – 基础篇(语法、数据类型、变量、常量、运算符)
- 2025-07-10 32位浮点数(32位浮点数是什么数据类型)
- 2025-07-10 含大量图文解析及例程 | Linux下的ELF文件、链接、加载与库(上)
- 2025-07-10 go语言学习-基本数据类型(go语言详解)
- 2025-07-10 火了!西门子PLC编程比较指令学习,赶快收藏
- 2025-07-10 了解 PLC 编程中的浮点数(了解 plc 编程中的浮点数怎么算)
- 2025-07-10 【Java教程】基础语法到高级特性(java高级用法)
- 2025-07-10 三菱plc的数据类型(PLC的基础)(三菱plc数据指令)
- 2025-07-10 计算机等级四级知识(计算机4级考试题目)
- 2025-07-10 200Smart数据类型之浮点数(smart浮点数转换为整数)
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- ifneq (61)
- 字符串长度在线 (61)
- googlecloud (64)
- messagesource (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)