前言
大家好,欢迎来到大厂面试系列,本文给大家介绍一下JS中大数运算和浮点数运算如何实现,并且详细讲解其中的知识点,包含了许多大厂喜欢追问的一些细节问题,请接着往下看吧!
大数运算
在JS中,当我们对一个非常大的数进行运算操作时(如下),会发现出现了各种各样的问题,那么,这是由什么导致的呢?
// 加法错误
console.log(9007199254740992 + 1); // 9007199254740992(应为 9007199254740993)
// 乘法错误
console.log(9007199254740992 * 2); // 18014398509481984(可能不准确)
// 比较错误
console.log(9007199254740992 === 9007199254740993); // true(错误)
安全范围
其实,JS的安全整数范围是−2^53+1到2^53−1,这个数值我们也称为Number.MAX_SAFE_INTEGER
我们可以通过JS查看其具体的值为9007199254740991
同时,你会发现,你尝试使用一个超出安全整数范围的数,编辑器会提示我们
而上面的各种运算错误以及比较错误就是由于9007199254740992超过了9007199254740991这个安全范围中的最大值。
如何处理大数运算呢?
那么,我们知道了不能用大于安全范围的值直接去计算,那么在实际业务中,倘若我们不得不对超出安全范围的值去计算,那么该怎么办呢?
手动模拟运算
实际上,我们会去手动模拟运算,比如计算两个大数num1和num2的和,我们可以通过以下方式模拟加法运算的程序
-
将num1和num2转化为字符串str1和str2
-
用两个指针指向str1和str2的最后一位
-
模拟加法运算:从后往前遍历两数代表的字符串,相加当前位的和,并且记录是否需要进位
function addStrings(num1, num2) {
let i = num1.length - 1; // 指针指向 num1 的末尾
let j = num2.length - 1; // 指针指向 num2 的末尾
let carry = 0; // 进位标志
let result = ''; // 存储结果
// 从右到左逐位相加
while (i >= 0 || j >= 0 || carry > 0) {
// 获取当前位的数字,若指针越界则用 0 代替
const digit1 = i >= 0 ? parseInt(num1[i--]) : 0;
const digit2 = j >= 0 ? parseInt(num2[j--]) : 0;
// 计算当前位的和(包括进位)
const sum = digit1 + digit2 + carry;
// 当前位的值
result = (sum % 10) + result
// 更新进位
carry = Math.floor(sum / 10);
}
return result
}
// 测试用例
console.log(addStrings("123", "456")); // "579"
console.log(addStrings("999", "1")); // "1000"
BigInt
我们能看见,上面的代码确实可行,在ES6之前,JS就是这样实现两个大数相加,很麻烦对不对,学过Python的大佬可能更深有体会,因为Python可以自动处理两个大数相加,在ES6之前,对于上面的这一大段代码,Python其实一行就能搞定。
但其实JS在被设计出来的目的是为了实现页面的展示效果和交互,它的目的是为了“表现”,而Python这种语言的目的是为了实现计算,所以我们的JS根本没有设计像Python中直接进行大数运算的这种功能。
但是,随着时代的推进,人们发现JavaScript在各类大型项目的应用越来越广泛,它各种“残缺”的功能也逐渐被人诟病,于是ES6之后,JavaScript就进行了一次大更新,推出了很多类似于块级作用域、箭头函数这样的各式各样的新特性,同时也推出了能帮我们快速实现大数运算的BigInt。
BigInt是一种简单的数据类型,能够表示安全范围外的数字。
1.BigInt的声明方式
BigInt有两种声明方式
//两种声明方式
const bigNum = 123456789012345678901234567890123456789n;
const theNum = BigInt("123456789012345678901234567890123456789"); //传参需要用字符串的方式
要注意,我们使用函数进行声明的时候,不能使用new BigInt哦,因为BigInt是原始类型(Primitive),而不是对象,而且在传参时需要传入字符串。
2.BigInt相加
有了两个BigInt数据类型的数据,咱们就可以放心对大数进行相加啦!
const BigNum1 = 500000000000000000000000000000n
const bigNum2 = BigInt("500000000000000000000000000000")
console.log(BigNum1,bigNum2,BigNum1 + bigNum2);
//500000000000000000000000000000n
//500000000000000000000000000000n
//1000000000000000000000000000000n
注意哦,相加的时候不能拿BigInt和普通的数字进行相加,只能用BigInt和BigInt相加。
3.BigInt的原理
其实BigInt实现的原理有点类似于我们上面的模拟加法运算。
BigInt 通常基于 数组或链表 存储每一位数字。(例如,12345678901234567890n 可能存储为 [1,2,3,...,0] 这样的数组。)
而底层的相加是模拟我们的加法运算进行相加,从最低位开始相加,如果某一位的和>=10,则进位,直到最高位。感兴趣的大佬可以去看一下源码,这里就简单介绍了。
拓展:Number.MAX_VALUE与Infinity
前面我们提到了,最大的安全数是MAX_SAFE_INTEGER(2^53-1),在JS中,对超出这个值的数进行计算会产生各种各样的计算问题。
Number.MAX_VALUE
Number.MAX_VALUE指的是JavaScript 中能表示的最大有限数值,约为1.7976931348623157e+308,注意:要将JS中的MAX_SAFE_INTEGER与Number.MAX_VALUE进行区分开来,前者指的是最大安全整数,超出这个整数进行计算会精度丢失,后者指的是最大有限数值。
Infinity
Infinity在JS中代表无穷大,是一个全局属性,它没有具体值,我们在某些算法题内可以使用它,因为任何有限值都小于Infinity,所以当求最小值的题目时,可以把它拿来当初始值用。
它们三者的区别如下
| 特性 | Number.MAX_SAFE_INTEGER | Number.MAX_VALUE | Infinity |
|---|---|---|---|
| 定义 | 最大安全整数(精确表示的整数上限) | 最大有限数值(浮点数上限) | 数学上的无穷大 |
| 具体值 | 9007199254740991(2⁵³ - 1) | ≈1.7976931348623157e+308 | Infinity(无具体值) |
| 用途 | 确保整数运算不丢失精度 | 表示浮点数的最大值 | 表示超出数值范围的无限概念 |
| 超出范围的结果 | 超出后可能丢失精度(如 +1 不准确) | 超出后变为 Infinity | 任何有限数 < Infinity |
小数运算:
0.1+0.2 是否等于 0.3 ?
大家请看如下场景
我们会发现,在JS中,0.1+0.2居然不是等于0.3,这到底是为什么呢,让我们一起来探讨一下这道有名的前端面试题吧!!
IEEE 754
在JS以及大多数编程语言中,都是采用IEE 754双精度浮点数(64位存储数字),其存储方式为:
- 1 位 符号位(
0正数,1负数) - 11 位 指数位(决定数值范围)
- 52 位 尾数位(决定数值精度)
二进制运算
一般十进制小数要先转换成二进制,比如0.625转换成0.101,但并不是所有小数都能转换成二进制的。0.1就不能直接用二进制表示,它的二进制是0.000110011001100…,这是一个无限循环小数。
所以结合我们上面的IEEE 754,0.1在用二进制表示时,会在52位时截断,由于IEE 754的尾数为只有52位,所以最终用一个52位的近似值来表示0.1,同样0.2也是,它们都会以一个二进制的近似值来表示。
所以上面0.1+0.2实际上是:
0.1的二进制近似值:
0.0001100110011001100110011001100110011001100110011001101 (52位截断)
0.2的二进制近似值:
0.001100110011001100110011001100110011001100110011001101 (52位截断)
- 两者相加后
0.0100110011001100110011001100110011001100110011001100111
4.相加结果转换成十进制
0.30000000000000004
所以我们才会看到0.1 + 0.2 === 0.30000000000000004,这都是由于浮点数的精度丢失问题
如何避免浮点数精度问题?
我们知道了为什么会0.1 + 0.2 !== 0.3 后,那么怎么做能得到我们想要的结果让0.1 + 0.2等于0.3呢,很可惜的是,这个历史遗留的问题至今没有得到彻底解决,我们只能通过间接的方式来使得0.1+0.2等于0.3。
第一种方式: 最简单的实现方式就是(0.1* 10 + 0.2 *10)/10==0.3,几乎所有语言都能使用这种方式来进行解决这个题。
第二种方式
使用 toFixed() 四舍五入(适用于显示)
const sum = 0.1 + 0.2; // 0.30000000000000004
const fixedSum = parseFloat(sum.toFixed(10)); // 0.3(保留10位小数并转回数字)
console.log(fixedSum === 0.3); // true
第三种方式 有些时候,我们需要控制一定的误差,也能达到我们的效果
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true
Number.EPSILON 是 JavaScript 最小精度值(约 2.22e-16),用于判断两个浮点数是否“足够接近”。
但是这种方式可能不适合一些需要很精确计算的业务,所以我们只需了解即可。
总结
JavaScript中的数值运算看似简单,却隐藏着许多细节问题,比如大数运算的精度丢失和浮点数计算的误差。本文从Number.MAX_SAFE_INTEGER的安全范围出发,探讨了大数运算的解决方案(如BigInt和手动模拟运算),并深入解析了经典的0.1 + 0.2 ≠ 0.3问题背后的IEEE 754浮点数存储机制。最后,提供了几种避免精度问题的方法,帮助你在实际开发中规避这些“坑”。无论是面试还是实际项目,掌握这些细节都能让你更加游刃有余!
🌇结尾
感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)
作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。