目录
一、背景
JavaScript 使用 IEEE 754 浮点数标准来表示和处理浮点数。IEEE 754 定义了两种浮点数表示形式:单精度浮点数(32 位)和双精度浮点数(64 位)。在 JavaScript 中,所有数字都被存储为双精度浮点数。
具体来说,JavaScript 中的 Number 类型采用 IEEE 754 双精度浮点数标准,它占用 64 位(8 字节)内存。其中:
- 1 位表示符号位(正负号)。
- 11 位表示指数部分。
- 52 位表示尾数(有效数字)。
这个表示方式使得 JavaScript 可以表示非常大或非常小的数值,同时具有足够的精度。
然而,由于使用二进制表示浮点数,有些十进制小数无法精确表示,可能导致一些精度损失。例如,0.1
(经典 0.1 + 0.2 !== 0.3
) 的二进制表示是一个无限循环小数,因此在计算机内部会存在一些精度误差。
这是浮点数表示中的一个常见问题,称为浮点数精度问题,在下一篇文章我们详细阐述精度丢失的来龙去脉。在需要高精度计算的场景中,可能需要使用特殊的库或方法来处理。
1.1 JavaScript Number 最值
Number.MIN_VALUE
,这个值在多数浏览器中是 5e-324
。它表示的是一个很接近于零的数,通常被称为 JavaScript 中的最小正非零数。它表示的是 5 乘以 10 的负 324 次方,即:5e-324 = 5 * 10^(-324)
。
Number.MAX_VALUE
,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308
。它表示的是 JavaScript 中可以表示的最大正数。它表示的是 1.7976931348623157 乘以 10 的 308 次方,即:1.7976931348623157e+308 = 1.7976931348623157 * 10^308
这些常量是 JavaScript 语言规范中定义的,并且它们分别代表了 IEEE 754 双精度浮点数的最大正数和最小正数。
请注意,JavaScript 中的浮点数表示是有限的,因此它们在表示极大或极小值时可能会失去精度。在处理需要非常大或非常小的数值时,要特别小心处理舍入误差和精度丢失。如果需要更高的精度,可以考虑使用 BigInt 类型或专门的数学库。
注意,如果超出表示范围,用一个特殊的 Infinity
值表示。
1.2 JavaScript Number 整数最值
在 JavaScript 中,有两个特殊的常量表示整数的最大和最小安全整数:
Number.MAX_SAFE_INTEGER
: 这个常量表示 JavaScript 中可以被精确表示的最大整数。它的值为 2^53 - 1,即9007199254740991
。
console.log(Number.MAX_SAFE_INTEGER); // 输出 9007199254740991
Number.MIN_SAFE_INTEGER
: 这个常量表示 JavaScript 中可以被精确表示的最小整数。它的值为 -2^53 + 1,即-9007199254740991
。
console.log(Number.MIN_SAFE_INTEGER); // 输出 -9007199254740991
这两个常量的命名中包含了 “SAFE” 一词,表示这是在 JavaScript 浮点数表示范围内的最大和最小整数,超过这个范围的整数可能会导致精度丢失。
如果你需要处理更大或更小的整数,可以考虑使用 BigInt
类型。 BigInt
类型没有上述整数范围的限制,因为它可以表示任意精度的整数。
1.3 JavaScript 正无穷大和负无穷大
JavaScript 中的正无穷大表示为 Infinity
,负无穷大表示为 -Infinity
。这两个值用来表示超出了 JavaScript 双精度浮点数范围的数值。
1.4 两个大数相加:溢出或精度丢失
在 JavaScript 中,如果两个大数相加导致结果超出了能够精确表示的最大正数范围,就会发生溢出和精度丢失。JavaScript 中的数值类型是双精度浮点数,能够表示的范围是有限的。当进行超出这个范围的运算时,会导致结果不准确。
下面是一个示例:
const maxSafeInteger = Number.MAX_SAFE_INTEGER; // 9007199254740991
const bigNumber1 = maxSafeInteger + 1; // 超出范围
const bigNumber2 = maxSafeInteger + 2; // 超出范围
console.log(bigNumber1 === maxSafeInteger); // 输出 true
console.log(bigNumber2 === maxSafeInteger); // 输出 true
在这个例子中,maxSafeInteger
是 JavaScript 能够精确表示的最大整数,它的值是 9007199254740991
。当我们尝试将其加上 1
或 2
时,结果超出了 JavaScript 能够精确表示的范围,因此导致了精度丢失。JavaScript 会将这些超出范围的结果截断为最大可表示的整数值,因此 bigNumber1
和 bigNumber2
的值都会变成 maxSafeInteger
,结果为 true
。
注意,在一些现代浏览器 bigNumber1 和 bigNumber2 能够正常计算,即最后的 === 返回为 false,符合预期。
看下一个例子:
const max = Number.MAX_VALUE;
const bigNumber1 = max + 1; // 超出范围
const bigNumber2 = max + 2; // 超出范围
console.log(bigNumber1 === max); // 输出 true
console.log(bigNumber2 === max); // 输出 true
这个例子就实实在在展示了两个超出范围的大数精度丢失的问题。
1.5 BigInt:任意精度的整数解决两个大整数相加
BigInt 可以存储非常大的整数,而不受常规 JavaScript 数字类型的限制,这是因为:BigInt 是一种新的数据类型,它不是基于 IEEE 754 浮点数标准的,而是表示任意精度的整数。
比如,在 JavaScript 中,常规的 Number 类型是基于浮点数的,遵循 IEEE 754 标准,它对整数有一个限制。JavaScript 中的数字使用 64 位双精度浮点数表示,其中包括一个符号位、11 位指数部分和 52 位尾数部分。这使得 JavaScript 能够精确表示的整数范围有上限,即 Number.MAX_SAFE_INTEGER
和 Number.MIN_SAFE_INTEGER
。
而 BigInt 是在 ECMAScript 2020 引入的一种新的数据类型,它被设计用于表示任意精度的整数。BigInt 的存储不依赖于浮点数表示,而是使用可变长度
的整数表示。这使得 BigInt 能够表示远超过常规整数范围的整数,而且不会有浮点数精度问题。
let bigIntResult = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1);
console.log(bigIntResult); // 9007199254740992n
使用 BigInt 类型,你可以更好地处理大整数的运算,而不会丢失精度。请注意,BigInt 类型是在 ECMAScript 2020 中引入的,因此需要确保你的运行环境支持该特性。
而且在理论上,BigInt
可以存储的整数大小是没有上限的,因为它不受浮点数的精度限制。在实际中,BigInt
的范围由计算机内存的限制所限制,也就是和说它可以动态地调整存储空间以容纳任意大的整数。
以下是一个简单的示例,演示了使用 BigInt
存储非常大的整数:
let largeNumber = BigInt("123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890");
console.log(largeNumber); // 12345678901234567890123456789012345678901234567890…345678901234567890123456789012345678901234567890n
在这个示例中,largeNumber
是一个 BigInt
类型的变量,存储了一个非常大的整数。这使得 BigInt
成为处理需要更大范围整数的计算或应用的有用工具。
1.6 两个大浮点数相加
对于浮点数的精度丢失和溢出问题,BigInt 并不能提供直接的解决方案。BigInt 主要用于处理大整数,而不是浮点数。对于浮点数的精度丢失和溢出问题,一般需要通过其他方式来解决,例如:
-
使用库或工具:有些库或工具提供了精确的十进制浮点数计算功能,可以避免 JavaScript 浮点数的精度丢失问题。例如,
decimal.js
库提供了高精度的十进制数计算功能。 -
手动实现精度控制:可以手动实现对浮点数的精度控制,例如将浮点数转换为整数进行计算,然后再将结果转换回浮点数。这种方法可以一定程度上避免精度丢失问题,但需要注意性能和精度的平衡。
-
避免超出范围的计算:在进行浮点数计算时,尽量避免超出 JavaScript 能够精确表示的范围,可以通过检查边界条件来预防溢出和精度丢失问题。
总的来说,处理浮点数的精度丢失和溢出问题需要根据具体的情况选择合适的方法,并且需要在性能、精度和代码复杂度之间进行权衡。
二、代码实现:避免大数相加溢出
通常可以将大数转换为字符串来进行相加操作。
2.1 两个大整数相加
思路:
-
- 补齐两个 num 长度
-
- 数字转字符,字符挨个相加
-
- 相加得到的值要区分进位
carry=Math.floor(sum/10)
和真实值(sum % 10)
- 相加得到的值要区分进位
const num1 = "987654321987654321";
const num2 = "9123456789";
const addLargeNumbers = (num1, num2) => {
let carry = 0; // 进位
let result = '';
let num1Str = '' + num1; // 数字转字符
let num2Str = '' + num2; // 数字转字符
const maxLength = Math.max(num1Str.length, num2Str.length);
num1Str = num1Str.padStart(maxLength, '0'); // padStart 补齐
num2Str = num2Str.padStart(maxLength, '0'); // padStart 补齐
console.log('num1: ', num1Str);
console.log('num2', num2Str);
for (let i = maxLength - 1; i >= 0; i--) {
let currentValue1 = Number.parseInt(num1Str[i], 10);
let currentValue2 = Number.parseInt(num2Str[i], 10);
let sum = (currentValue1 + currentValue2) + carry; // sum 公式
result = (sum % 10) + result; // 累加
carry = Math.floor(sum / 10); // 进位
}
if (carry > 0) {
result = carry + result;
}
return result;
}
const sum = addLargeNumbers(num1, num2);
console.log(sum); // 987654331111111110
2.2 两个大浮点数相加
在上述实现基础上:
首先将两个浮点数转换为字符串,并分割出整数部分和小数部分。然后补齐小数部分的长度,使两个浮点数的小数部分长度相等。接着按照整数部分和小数部分分开相加的逻辑进行处理,最后将结果转换为浮点数返回。
const num1 = "987654321987654321.456";
const num2 = "9123456789.712345";
const addLargeNumbers = (num1, num2) => {
// 将浮点数转换为字符串,并分割整数部分和小数部分
let [intPart1, decPart1] = num1.toString().split('.');
let [intPart2, decPart2] = num2.toString().split('.');
const maxLength = Math.max(intPart1.length, intPart2.length);
intPart1 = intPart1.padStart(maxLength, '0'); // padStart 补齐
intPart2 = intPart2.padStart(maxLength, '0'); // padStart 补齐
// 注意小数点之后可能没有小数值
const maxLengthDec = Math.max(decPart1 ? decPart1.length : 0, decPart2 ? decPart2.length : 0);
decPart1 = decPart1 ? decPart1.padEnd(maxLengthDec, '0') : '';
decPart2 = decPart2 ? decPart2.padEnd(maxLengthDec, '0') : '';
let carry = 0; // 进位
let result = '';
console.log('num1: ', intPart1, decPart1); // 987654321987654321 456000
console.log('num2', intPart2, decPart2); // 000000009123456789 112345
// 将整数部分和补齐后的小数部分合并为字符串
const num1Str = intPart1 + decPart1;
const num2Str = intPart2 + decPart2;
// 从小数点后第一位开始向前遍历相加
for (let i = num1Str.length - 1; i >= 0; i--) {
let currentValue1 = Number.parseInt(num1Str[i], 10);
let currentValue2 = Number.parseInt(num2Str[i], 10);
let sum = (currentValue1 + currentValue2) + carry; // sum 公式
result = (sum % 10) + result; // 累加
carry = Math.floor(sum / 10); // 进位
}
if (carry > 0) {
result = carry + result;
}
return result.slice(0, maxLength) + '.' + result.slice(maxLength);
}
const sum = addLargeNumbers(num1, num2);
console.log(sum); // 987654331111111111.168345