JS 精度问题是一个经典面试问题,不管是对 Javascript 语言细节还是对业务熟练度的考察,它都非常适合,也是中小企业以及大厂面试题库必备。
1. 问题现象
1.1 浮点数精度问题
javascript
console.log(0.1 + 0.2); // 输出 0.30000000000000004
console.log(0.1 + 0.7); // 输出 0.7999999999999999
1.2 整数精度问题
javascript
console.log(9007199254740991 + 1); // 输出 9007199254740992(正确)
console.log(9007199254740991 + 2); // 输出 9007199254740992(错误)
2. 中小公司会怎么问?
2.1. 0.1 + 0.2 输出什么以及为什么?
浮点数在计算机中以二进制表示,某些十进制小数(如 0.1 和 0.2)无法精确表示,导致运算结果不精确。
2.2. JS Numer 类型的数值范围?
JavaScript 中的 Number
类型能表示的安全整数范围是 (-2的53次方) - (2的53次方 - 1),超过这个范围可能会丢失精度。
2.3. 如何解决数值精度问题?
(1)避免浮点运算,转换为整数再计算,这种方式比较麻烦
javascript
const result = (0.1 * 10 + 0.2 * 10) / 10;
console.log(result); // 输出 0.3
(2)借助第三库解决,推荐使用 decimal.js www.npmjs.com/package/dec... 等库。
javascript
const Decimal = require('decimal.js');
console.log(new Decimal(0.1).plus(0.2).toString()); // 输出 "0.3"
2.4. 为什么后端返回的 int64 值,前端收到不准确
1)后端通常将数据序列化为 JSON 格式字符串,发送给前端。
2)前端接收到 JSON 数据后,通过 window.JSON.parse 序列化,其中的 int64
字段被解析为 JS 的 Number
类型。
3)如果 int64
的值超出了安全范围,会导致精度丢失。
需要重写 window.JSON.parse,比如借助 jsonbigint www.npmjs.com/package/jso... ,或许 axios 库真应该支持自定义 parse 功能 😂。
3.3. 大厂会怎么问
大厂一般会循序渐进,从简单到困难逐渐深入。
3.1. 0.1 + 0.2 = 0.3 吗?
浮点数在计算机中以二进制表示,某些十进制小数(如 0.1 和 0.2)无法精确表示,导致运算结果不精确。
3.2 浮点数是怎么被表示的
IEEE二进制浮点数算术标准 baike.baidu.com/item/IEEE%2...,如果你理解起来有困难,可以在群里群主,我们有专门的人免费解答。
在 二进制 中,数字只能用 2 的幂次 来表示,浮点数使用科学计数法表示,例如:
在这种表示方法下,小数只能用 2的负幂来表示。
比如将 0.125 转换成二进制小数,我们需要不断将小数部分乘以 2,并记录整数部分,直到小数部分为 0 或者达到所需精度。
javascript
0.125 * 2 // 整数部分0, 小数部分 0.25
0.25 * 2 // 整数部分0, 小数部分 0.5
0.5 * 2 // 整数部分1, 小数部分 0
计算完成因此,0.125 的二进制为 0.001 采用科学计数法:符号位是 0,指数为 -3, 尾数位为0
javascript
0.125 = 2^-3 * 1.0
我们再来计算 0.1 的二进制数
javascript
0.1 * 2 // 整数部分0, 小数部分 0.2
0.2 * 2 // 整数部分0, 小数部分 0.4
0.4 * 2 // 整数部分0, 小数部分 0.8
0.8 * 2 // 整数部分1, 小数部分 0.6 // 标记M1
0.6 * 2 // 整数部分1, 小数部分 0.2
0.2 * 2 // 整数部分0, 小数部分 0.4
0.4 * 2 // 整数部分0, 小数部分 0.8
0.8 * 2 // 整数部分1, 小数部分 0.6 // 又回到M1
所以他会导致无限循环计算,最终表示 0.000110011(0011)一直循环,采用科学计数法时不能正确的表示,因为尾数位的长度是有限的,所以 0.1 实际存储的是一个近似值,而不是精确的二进制表示。在 IEEE 754 双精度浮点数结构中,尾数位的长度是 52 位。
3.3 大数相加的实现
这个考察有没有去思考第三方库是怎么实现的,不要只是会用,还要明白其中的原理。
核心思路就是模拟算术过程
主要实现代码:
javascript
function addStrings(strA, strB) {
if (strA === "0") return strB;
if (strB === "0") return strA;
var m = strA.length;
var n = strB.length;
var maxLen = Math.max(m, n);
var list = (new Array(maxLen + 1)).fill(0);
// 倒序填充 arrA
var arrA = new Array(maxLen + 1).fill(0);
for (let i = 0; i < m; i++) {
arrA[i] = Number(strA[m - i - 1]);
}
// 倒序填充 arrB
var arrB = new Array(maxLen + 1).fill(0);
for (let i = 0; i < n; i++) {
arrB[i] = Number(strB[n - i - 1]);
}
// 计算结果填充到 list
for (let i = 0; i < list.length; i++) {
var t = list[i];
t += arrA[i];
t += arrB[i];
if (t > 9) {
t = t - 10;
list[i + 1] = 1;
}
list[i] = t;
}
// 转为字符串
var strRet = "";
let findFirst = false;
for (let i = list.length - 1; i >= 0; i--) {
if (findFirst) {
strRet += list[i].toString();
} else {
if (list[i] === 0) {
continue;
}
findFirst = true;
strRet += list[i].toString();
}
}
return strRet;
}
// 用例
console.log(addStrings("5555", "6666"))
浮点数相加
将整数部分 和 小数部分 提取出来分别计算,如果新的小数的位数比两者都要大,说明整数部分需要进1,而小数位需要去掉第一位。然后返回二者的拼接。
javascript
function numberSum(strA, strB){
if(!(strA.includes(".")|| strB.includes("."))){
// 整数
return bigNumberSum(strA, strB)
}
// 整数部分
var strAInt = (strA.split('.'))[0];
var strBInt = (strB.split('.'))[0];
var strInt = bigNumberSum(strAInt, strBInt);
// 小数部分
var strADot = (strA.split('.'))[1];
var strBDot = (strB.split('.'))[1];
// 小数对其
var max = Math.max(strADot.length, strBDot.length);
for(let i =0; i < max; i++){
if(strADot[i] === undefined) strADot += "0";
if(strBDot[i] === undefined) strBDot += "0";
}
var strDot = bigNumberSum(strADot, strBDot);
if(strDot.length > max){
strInt = bigNumberSum(strInt,"1");
strDot = strDot.substr(1);
}
return strInt +"."+ strDot
}
// 用例
var a = "12345678932131.55555";
var b = "98765432156678.5444";
var r = numberSum(a, b);
console.log(`${a} + ${b}`);
console.log(r);
console.log(Number(a) + Number(b));
3.4 如何解决相关性能问题?
JS 不擅长计算密集型,比如字符串处理(JSON.parse)就属于计算密集型,在使用第三方库时比如 json-bigint 会遇到性能问题,该如何解决?
可以基于一些高性能的库(C或者C++),自定义魔改打包为 Wasm 给浏览器使用