在 JavaScript 中处理数字,表面看起来和别的语言一样简单:写个 123 就是整数,写个 3.14 就是小数。但一旦涉及金额计算、大数 ID、高精度运算,很多开发者都会掉进同一个坑里。这一节会从 JavaScript 的数字底层表示出发,讲清楚精度问题的根源、数学计算的常用工具,以及 ES2020 引入的 BigInt 如何弥补 Number 的长整数短板。
10.3.1 JavaScript 中的数字:全都是 IEEE 754 双精度浮点数
JavaScript 内部只有一种数值类型——Number。无论你写的是整数还是小数,底层都以 64 位双精度浮点数(IEEE 754 标准)存储。这 64 位的分配是:
- 1 位 符号位(sign)
- 11 位 指数位(exponent)
- 52 位 尾数位(fraction / mantissa)
整个数值的计算公式为:
\[
value = (-1)^{sign} \times (1 + fraction) \times 2^{exponent - 1023}
\]
其中尾数部分实际上隐含了一个前置的 1(也就是规格化形式),因此有效精度约为 15-17 位十进制数字。
记住一个关键结论:JavaScript 的 Number 只能安全表示范围在
-(2^53 - 1)到2^53 - 1之间的整数(即 ±9,007,199,254,740,991)。超出这个范围的整数,计算会丢失精度。
整数范围
const maxSafe = Number.MAX_SAFE_INTEGER; // 9007199254740991
const minSafe = Number.MIN_SAFE_INTEGER; // -9007199254740991
console.log(maxSafe + 1 === maxSafe + 2); // true,因为加 1 和加 2 的结果相同,精度丢失
对于更大的整数(如某些后端返回的雪花 ID、社交平台的帖子 ID),使用 Number 会导致失真。此时应使用后面会讲到的 BigInt 或将其作为字符串处理。
10.3.2 经典精度问题:为什么 0.1 + 0.2 ≠ 0.3
这句面试常见问题几乎成了 JavaScript 的刻板印象,但它背后的原因纯属数学,不属于语言设计缺陷。因为十进制小数在转换成二进制时,很多值(如 0.1、0.2)会变成无限循环小数(类似十进制中的 1/3)。而 IEEE 754 只有 52 位尾数来表示小数部分,必然要进行舍入,导致误差。
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // false
日常应对方案:
- 转为整数运算
将小数放大成整数,计算完再缩回去。例如处理金额时,统一以“分”为单位(整数),避免浮点数直接参与计算。
(0.1 * 10 + 0.2 * 10) / 10; // 0.3
- 使用
toFixed()或toPrecision()进行格式化输出
这两个方法会返回字符串,适合展示,但不适合进一步精确计算。
(0.1 + 0.2).toFixed(1); // "0.3"
- 借助专门的库
金额计算推荐 decimal.js 或 big.js,避免自己裸写小数运算。
- ES6 引入的
Number.EPSILON辅助判断相等
它表示 1 与大于 1 的最小浮点数之间的差值,可用于误差容忍比较。
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
isEqual(0.1 + 0.2, 0.3); // true
但这个方案不适用于所有情形,尤其是需要有意义的绝对误差控制时。
10.3.3 Number 的边界值与实用方法
除了 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER,Number 对象还定义了几个边界值:
Number.MAX_VALUE:最大正浮点数,约1.79e+308Number.MIN_VALUE:最小正浮点数,约5e-324Number.POSITIVE_INFINITY(Infinity)Number.NEGATIVE_INFINITY(-Infinity)Number.NaN:非数值
实用静态方法和实例方法:
Number.isInteger(value):判断是否为安全的整数(注意它不是判断是否为Number类型)Number.isFinite(value):判断是否为有限数(排除Infinity、-Infinity、NaN)Number.isNaN(value):比全局isNaN更可靠,只在值为NaN时返回trueparseInt(string, radix)/parseFloat(string):从字符串解析数字,全局方法,非 Number 独有,但常一起使用。number.toString(radix):将数字转换为指定进制的字符串。number.toFixed(digits):保留指定小数位数,返回字符串。number.toPrecision(precision):按指定有效数字位数返回字符串。
10.3.4 Math 对象常用方法
JavaScript 内置的 Math 对象不是一个构造函数,直接作为静态工具使用,涵盖取整、随机数、三角函数、指数对数等。
取整与绝对值
Math.floor(x):向下取整,Math.floor(3.9)→3Math.ceil(x):向上取整,Math.ceil(3.1)→4Math.round(x):四舍五入取整(注意负数时.5向零舍入)Math.trunc(x):直接去除小数部分(ES6),Math.trunc(-3.9)→-3Math.abs(x):绝对值
最值、随机数、符号
Math.max(...values)/Math.min(...values):传入多个参数,可结合展开运算符处理数组。Math.random():返回[0, 1)之间的伪随机数。若要生成某个范围内的随机整数,常用公式:
// [min, max] 之间的随机整数
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Math.sign(x):返回 1(正)、-1(负)、0、-0 或 NaN。
幂和开方
Math.pow(base, exponent):等同于base ** exponent运算符(ES2016)Math.sqrt(x):平方根Math.cbrt(x):立方根(ES6)Math.hypot(...values):返回所有参数平方和的平方根,常用于计算欧几里得距离。
三角函数(弧度制)
Math.PI:π 常量Math.sin(x),Math.cos(x),Math.tan(x)Math.asin(x),Math.acos(x),Math.atan(x)Math.atan2(y, x):常用于根据坐标计算角度。
对数与指数
Math.log(x):自然对数Math.log10(x):以 10 为底的对数Math.log2(x):以 2 为底的对数Math.exp(x):e 的 x 次方
大部分 Math 方法的参数如果不是数值,会隐式调用 Number() 转换,无法转换则返回 NaN。
10.3.5 BigInt:任意精度的整数
ES2020 引入的 BigInt 彻底解决了超大整数的表示和运算问题。BigInt 不是 Number,它是一种全新的原始类型,可以表示任意大小的整数(受限于内存),且不会丢失精度。
创建 BigInt
- 字面量:在整数后加
n
const big = 9007199254740993n;
- 构造函数:
BigInt(),可以传入数字或字符串。
const bigFromNum = BigInt(123);
const bigFromStr = BigInt("9007199254740993");
注意传入的数字不能是 Number 安全范围之外的值,否则会先在 Number 阶段失真,建议直接用字符串。
BigInt 运算
- 支持常规算术:
+、-、、/、%、*。 - 除法会向零截断(返回整数 BigInt),不会产生小数。
5n / 2n; // 2n
- 不能与 Number 混合运算,必须显式转换。
const a = 10n;
const b = 20;
a + BigInt(b); // 30n
// a + b // TypeError
- 比较运算可以跨类型,
0n === 0为 false(类型不同),但0n == 0为 true(宽松相等)。
常用场景
- 长整数 ID(例如 Twitter 的 snowflake 算法生成的 64 位 ID)
- 金融中需绝对精确的整数(如计算以分为单位的金额)
- 密码学、高精度时间戳等
注意事项
JSON.stringify()不支持 BigInt,会抛出错误。发送到后端时可手动转为字符串。Math对象的方法不适用于 BigInt,不能混合调用。- 多数 Web API 仍然返回 Number,需根据情况转换。
10.3.6 实际开发建议
- 金额运算:要么用整数(分)计算,要么使用 decimal.js 等专业库,绝对不要直接用浮点数累加累减。
- 后端返回的长整型 ID:前端接收时如果 ID 超过
Number.MAX_SAFE_INTEGER,应让后端以字符串形式返回,或者前端收到后用 BigInt 处理(前提是浏览器支持)。 - 随机整数:牢记生成公式,封装成工具函数,避免到处手写。
- 小数显示:
toFixed()能满足大多数格式化需求,但注意它返回字符串。 - 比较浮点相等:慎用
===,考虑误差容忍或转为整数比较。
理解 JavaScript 数字特性的底层缘由,能帮你避开那些看似诡异的坑,写出更健壮的代码。下一章将进入字符串与正则表达式领域,它们与数字一起构成了日常开发中最常用的几种基本类型。