Menu

JavaScript 数字与 BigInt:浮点数、精度和大整数

JavaScript 的 Number 类型实际上是怎么工作的:浮点数精度、MAX_SAFE_INTEGER,以及什么时候应该改用 BigInt。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

几乎所有数字都用同一种类型

大多数语言会为整数和浮点数提供不同的类型。而 JavaScript 历来只给你一种:Number。不管你写的是 42、3.14 还是 -0.001,得到的都是同一种原始值:一个 64 位 IEEE 754 双精度浮点数。

这很方便:不需要在 int 和 float 之间转换,也不会在 2^31 处溢出。但浮点表示是有代价的,初学者经常因此踩坑。2020 年,JavaScript 加入了第二种数值类型 BigInt,用来处理 Number 力不从心的情况。

浮点数带来的意外

运行下面的代码:

第一行打印 0.30000000000000004,第二行打印 false。这不是 JavaScript 的怪癖,Python、Java、C,以及任何使用 IEEE 754 浮点数的语言都是这样。

原因在于:0.1 和 0.2 无法用二进制精确表示,就像 1/3 无法用十进制精确写出一样。存储的是最接近的二进制近似值,微小的误差会不断累积。可以这样理解:把带小数的 Number 值看作近似值,只是恰好非常接近你写的数。

处理金额时,不要把 $19.99 存成 19.99。把以分为单位的金额存成整数 1999,显示时再格式化。这是避开浮点数 bug 最好的一个习惯。

安全地比较浮点数

既然相等比较靠不住,需要时就用一个容差来比较:

Number.EPSILON 是 1 与下一个可表示的数之间的最小差值,对于接近 1 的值,它是一个合理的默认容差。对于非常大或非常小的数,你需要一个随输入大小缩放的容差。

安全整数范围

在 64 位浮点数中,一定范围内的整数是精确的。超出这个范围,精度就会一位一位地丢失:

2^53 - 1 是最后一个“它以下的每个整数都能被表示”的整数。超过它之后,有些整数在 Number 类型中根本不存在,会被舍入到最近的相邻值。如果你把数据库中的 64 位 ID 当作普通 JSON 数字来解析,这就是一个随时可能发生、悄无声息的数据损坏 bug。

BigInt 登场

BigInt 是一种单独的原始类型,用于任意精度的整数。在整数字面量后面加上 n,或者调用 BigInt(...),就能创建一个 BigInt:

BigInt 除了可用内存之外没有上限。它适合用于:

  • 超过 2^53 的数据库 ID 或 Twitter/X 的雪花 ID。
  • 密码学运算。
  • 任何精确结果比运行速度更重要的整数运算。

它不适合日常的计数器、数组索引,或以分为单位的金额:普通的 Number 更快,而且能和语言中的所有 API 配合使用。

BigInt 运算

只要两个操作数都是 BigInt,所有常用的运算符都可以使用:

除法会向零截断:不存在带小数的 BigInt。如果需要小数,就只能回到 Number(或者使用十进制库)。

不要混用类型

让人犯错的规则是:同一个表达式里不能混用 Number 和 BigInt。

比较运算是唯一的例外:<、>、== 会跨类型进行转换:

所以 == 认为它们相等,=== 则不然。如果你已经在所有地方都使用 ===(你应该这么做),那么就把两种类型之间的数值比较看作设计上的隐患:选定一边,然后进行转换。

两者之间的转换

两种转换,两个陷阱:

Number → BigInt 的转换很严格:小数和 NaN 都会抛出异常。BigInt → Number 的转换很宽松,但会丢失精度:超过 MAX_SAFE_INTEGER 的值都会被舍入。如果你要转换从服务器收到的 BigInt,先问问自己是否真的需要转换。

特殊的数字值

顺便说一下,Number 类型中有三个值在数学意义上并不是数字:

除以零或超出浮点数范围时,会出现 Infinity 和 -Infinity。当算术运算得不到有意义的结果时,会出现 NaN(“not a number”,不是数字)。

NaN 不等于它自己,这一点人尽皆知:这是 IEEE 754 规范的一部分,不是 JS 的 bug。要用 Number.isNaN(x) 来判断。老的全局函数 isNaN 会先转换参数,因此会给出错误的答案(isNaN("hello") 返回 true)。永远优先使用 Number.isNaN。

从字符串解析数字

用户输入和 JSON 中的数字经常以字符串形式到来。有三种转换方式:

Number() 很严格:任何非数字内容都会得到 NaN,但空字符串和空白字符例外,它们会得到 0。parseInt 和 parseFloat 很宽容:能读多少就读多少,然后停下。选择符合你意图的那个,并在使用结果前检查是否为 NaN。

要从字符串解析 BigInt,用 BigInt("123"):它很严格,遇到错误的输入会抛出异常。

简明规则

  • 计数器、数学运算、坐标以及大多数日常数字:用 Number。
  • 金额:换算成以分为单位的整数并使用 Number,或者使用十进制库。
  • 大于 2^53 的整数(数据库 ID、密码学、组合数学):用带 n 后缀的 BigInt。
  • 用容差比较浮点数,而不是用 ===。
  • 用 Number.isNaN 和 Number.isFinite 检查无效结果,而不是用全局版本。
  • 不要在同一个表达式中混用 Number 和 BigInt,要显式转换。

下一章:null 与 undefined

JavaScript 有两种表示“没有值”的方式:null 和 undefined,而且它们不能互换。下一章讲解它们各自的含义、区别,以及什么时候该用哪一个。

常见问题

为什么 JavaScript 中 0.1 + 0.2 不等于 0.3?

因为 JavaScript 的 Number 类型是 64 位 IEEE 754 浮点数,而 0.1 和 0.2 无法用二进制精确表示。结果是 0.30000000000000004。这不是 JavaScript 的 bug,Python、Java 以及所有使用同样浮点格式的语言都会这样。处理金额时,把它换算成整数(以分为单位),或者使用十进制库。

JavaScript 中的 BigInt 是什么,什么时候该用它?

BigInt 是一种单独的数值原始类型,用于超出 Number.MAX_SAFE_INTEGER(2^53 - 1)的整数。在整数后面加 n 即可创建,如 9007199254740993n,也可以用 BigInt(value)。它适合 64 位数据库 ID、密码学,以及任何精度比速度更重要的整数运算。

JavaScript 中可以混用 Number 和 BigInt 吗?

不可以。1n + 1 会抛出 TypeError: Cannot mix BigInt and other types。要用 BigInt(n) 或 Number(b) 显式转换。< 和 == 这样的比较运算符可以跨两种类型使用,但 === 会返回 false,因为类型不同。

Coddy 编程语言插图

和 Coddy 一起学编程

开始