写给没耐心程序员的 JavaScript (ES2022 版)
请支持本书:购买捐赠
(广告,请不要屏蔽。)

18 Bigint – 任意精度整数 [ES2020] (高级)



在本章中,我们将介绍 bigint,它是 JavaScript 中的一种整数类型,其存储空间会根据需要增长和缩减。

18.1 为什么需要 bigint?

在 ECMAScript 2020 之前,JavaScript 如下处理整数:

有时,我们需要超过有符号 53 位的精度,例如:

18.2 Bigint

*Bigint* 是一种新的整数基本数据类型。Bigint 没有固定的位存储大小;它们的大小会根据它们表示的整数进行调整:

bigint 字面量是一个或多个数字序列,后缀为 n,例如:

123n

运算符(如 -*)已重载,可用于 bigint:

> 123n * 456n
56088n

Bigint 是基本类型值。typeof 为它们返回一个新的结果:

> typeof 123n
'bigint'

18.2.1 突破整数的 53 位限制

JavaScript 数字在内部表示为分数乘以指数(有关详细信息,请参阅§16.8 “背景:浮点精度”)。因此,如果我们超过最大的*安全整数* 253-1,仍然可以表示*一些*整数,但它们之间存在间隙:

> 2**53 - 2 // safe
9007199254740990
> 2**53 - 1 // safe
9007199254740991

> 2**53 // unsafe, same as next integer
9007199254740992
> 2**53 + 1
9007199254740992
> 2**53 + 2
9007199254740994
> 2**53 + 3
9007199254740996
> 2**53 + 4
9007199254740996
> 2**53 + 5
9007199254740996

Bigint 使我们能够突破 53 位的限制:

> 2n**53n
9007199254740992n
> 2n**53n + 1n
9007199254740993n
> 2n**53n + 2n
9007199254740994n

18.2.2 示例:使用 bigint

以下是使用 bigint 的示例(代码基于提案中的示例):

/**
 * Takes a bigint as an argument and returns a bigint
 */
function nthPrime(nth) {
  if (typeof nth !== 'bigint') {
    throw new TypeError();
  }
  function isPrime(p) {
    for (let i = 2n; i < p; i++) {
      if (p % i === 0n) return false;
    }
    return true;
  }
  for (let i = 2n; ; i++) {
    if (isPrime(i)) {
      if (--nth === 0n) return i;
    }
  }
}

assert.deepEqual(
  [1n, 2n, 3n, 4n, 5n].map(nth => nthPrime(nth)),
  [2n, 3n, 5n, 7n, 11n]
);

18.3 Bigint 字面量

与数字字面量一样,bigint 字面量支持多种进制:

负 bigint 通过在前面加上一元减号运算符来生成:-0123n

18.3.1 在 bigint 字面量中使用下划线 (_) 作为分隔符 [ES2021]

与数字字面量一样,我们可以在 bigint 字面量中使用下划线 (_) 作为分隔符:

const massOfEarthInKg = 6_000_000_000_000_000_000_000_000n;

Bigint 通常用于金融技术领域表示货币。分隔符在这里也很有帮助:

const priceInCents = 123_000_00n; // 123 thousand dollars

与数字字面量一样,适用两条限制:

18.4 对 bigint 重用数字运算符(重载)

对于大多数运算符,我们不允许混合使用 bigint 和数字。如果这样做,则会抛出异常:

> 2n + 1
TypeError: Cannot mix BigInt and other types, use explicit conversions

此规则的原因是没有通用的方法可以将数字和 bigint 强制转换为通用类型:数字不能表示超过 53 位的 bigint,bigint 不能表示分数。因此,异常会警告我们可能导致意外结果的拼写错误。

例如,以下表达式的结果应该是 9007199254740993n 还是 9007199254740992

2**53 + 1n

也不清楚以下表达式的结果应该是什么:

2n**53n * 3.3

18.4.1 算术运算符

二元 +、二元 -*** 按预期工作:

> 7n * 3n
21n

可以混合使用 bigint 和字符串:

> 6n + ' apples'
'6 apples'

/% 向零舍入(类似于 Math.trunc()):

> 1n / 2n
0n

一元 - 按预期工作:

> -(-64n)
64n

不支持对 bigint 使用一元 +,因为很多代码都依赖它将其操作数强制转换为数字:

> +23n
TypeError: Cannot convert a BigInt value to a number

18.4.2 比较运算符

比较运算符 <>>=<= 按预期工作:

> 17n <= 17n
true
> 3n > -1n
true

比较 bigint 和数字不会带来任何风险。因此,我们可以混合使用 bigint 和数字:

> 3n > -1
true

18.4.3 位运算符

18.4.3.1 数字的位运算符

位运算符将数字解释为 32 位整数。这些整数可以是有符号的,也可以是无符号的。如果它们是有符号的,则整数的负数是其*二进制补码*(将一个整数与其二进制补码相加(忽略溢出)将产生零):

> 2**32-1 >> 0
-1

由于这些整数具有固定大小,因此它们的最高位表示它们的符号:

> 2**31 >> 0 // highest bit is 1
-2147483648
> 2**31 - 1 >> 0 // highest bit is 0
2147483647
18.4.3.2 Bigint 的位运算符

对于 bigint,位运算符将负号解释为无限的二进制补码,例如:

也就是说,负号更像是一个外部标志,而不是表示为实际的位。

18.4.3.3 按位非 (~)

按位非 (~) 反转所有位:

> ~0b10n
-3n
> ~0n
-1n
> ~-2n
1n
18.4.3.4 二元位运算符 (&|^)

对 bigint 应用二元位运算符类似于对数字应用它们:

> (0b1010n |  0b0111n).toString(2)
'1111'
> (0b1010n &  0b0111n).toString(2)
'10'

> (0b1010n | -1n).toString(2)
'-1'
> (0b1010n & -1n).toString(2)
'1010'
18.4.3.5 带符号位移运算符 (<<>>)

bigint 的带符号位移运算符保留数字的符号:

> 2n << 1n
4n
> -2n << 1n
-4n

> 2n >> 1n
1n
> -2n >> 1n
-1n

回想一下,-1n 是一个无限向左扩展的 1 序列。这就是为什么将其左移不会改变它的原因:

> -1n >> 20n
-1n
18.4.3.6 无符号右移运算符 (>>>)

bigint 没有无符号右移运算符:

> 2n >>> 1n
TypeError: BigInts have no unsigned right shift, use >> instead

为什么?无符号右移背后的想法是将零“从左侧”移入。换句话说,假设二进制位数是有限的。

但是,对于 bigint,没有“左侧”,它们的二进制位数是无限扩展的。这对于负数尤其重要。

带符号右移即使在无限位数的情况下也能正常工作,因为它保留了最高位。因此,它可以适用于 bigint。

18.4.4 宽松相等 (==) 和不相等 (!=)

宽松相等 (==) 和不相等 (!=) 会强制转换值:

> 0n == false
true
> 1n == true
true

> 123n == 123
true

> 123n == '123'
true

18.4.5 严格相等 (===) 和不相等 (!==)

严格相等 (===) 和不相等 (!==) 仅在值具有相同类型时才认为它们相等:

> 123n === 123
false
> 123n === 123n
true

18.5 包装器构造函数 BigInt

与数字类似,bigint 也有关联的包装器构造函数 BigInt

18.5.1 BigInt 作为构造函数和函数

表 13:将值转换为 bigint。
x BigInt(x)
undefined 抛出 TypeError
null 抛出 TypeError
boolean false 0n, true 1n
number 示例:123 123n
非整数 抛出 RangeError
bigint x(无变化)
string 示例:'123' 123n
无法解析 抛出 SyntaxError
symbol 抛出 TypeError
object 可配置(例如,通过 .valueOf()
18.5.1.1 转换 undefinednull

如果 xundefinednull,则会抛出 TypeError

> BigInt(undefined)
TypeError: Cannot convert undefined to a BigInt
> BigInt(null)
TypeError: Cannot convert null to a BigInt
18.5.1.2 转换字符串

如果字符串不表示整数,则 BigInt() 会抛出 SyntaxError(而 Number() 返回错误值 NaN):

> BigInt('abc')
SyntaxError: Cannot convert abc to a BigInt

不允许使用后缀 'n'

> BigInt('123n')
SyntaxError: Cannot convert 123n to a BigInt

允许使用 bigint 字面量的所有进制:

> BigInt('123')
123n
> BigInt('0xFF')
255n
> BigInt('0b1101')
13n
> BigInt('0o777')
511n
18.5.1.3 非整数数字会产生异常
> BigInt(123.45)
RangeError: The number 123.45 cannot be converted to a BigInt because
it is not an integer
> BigInt(123)
123n
18.5.1.4 转换对象

如何将对象转换为 bigint 是可以配置的,例如,通过覆盖 .valueOf()

> BigInt({valueOf() {return 123n}})
123n

18.5.2 BigInt.prototype.* 方法

BigInt.prototype 包含基本 bigint“继承”的方法:

18.5.3 BigInt.* 方法

18.5.4 类型转换和 64 位整数

类型转换允许我们创建具有特定位数的整数值。如果我们想将自己限制为仅使用 64 位整数,则必须始终进行类型转换:

const uint64a = BigInt.asUintN(64, 12345n);
const uint64b = BigInt.asUintN(64, 67890n);
const result = BigInt.asUintN(64, uint64a * uint64b);

18.6 将 bigint 强制转换为其他基本类型

下表显示了将 bigint 转换为其他基本类型时会发生什么:

转换为 显式转换 强制转换(隐式转换)
boolean Boolean(0n) false !0n true
Boolean(int) true !int false
number Number(7n) 7(示例) +int TypeError (1)
string String(7n) '7'(示例) ''+7n '7'(示例)

脚注

18.7 用于 64 位值的 TypedArray 和 DataView 操作

由于 bigint 的出现,类型化数组和 DataView 现在可以支持 64 位值。

18.8 BigInt 和 JSON

JSON 标准是固定的,不会改变。好处是旧的 JSON 解析代码永远不会过时。缺点是 JSON 不能扩展为包含 bigint。

将 bigint 字符串化会抛出异常

> JSON.stringify(123n)
TypeError: Do not know how to serialize a BigInt
> JSON.stringify([123n])
TypeError: Do not know how to serialize a BigInt

18.8.1 将 bigint 字符串化

因此,我们最好的选择是将 bigint 存储在字符串中

const bigintPrefix = '[[bigint]]';

function bigintReplacer(_key, value) {
  if (typeof value === 'bigint') {
    return bigintPrefix + value;
  }
  return value;
}

const data = { value: 9007199254740993n };
assert.equal(
  JSON.stringify(data, bigintReplacer),
  '{"value":"[[bigint]]9007199254740993"}'
);

18.8.2 解析 bigint

以下代码展示了如何解析字符串,例如我们在上一个示例中生成的字符串。

function bigintReviver(_key, value) {
  if (typeof value === 'string' && value.startsWith(bigintPrefix)) {
    return BigInt(value.slice(bigintPrefix.length));
  }
  return value;
}

const str = '{"value":"[[bigint]]9007199254740993"}';
assert.deepEqual(
  JSON.parse(str, bigintReviver),
  { value: 9007199254740993n }
);

18.9 常见问题解答:BigInt

18.9.1 我如何决定何时使用数字,何时使用 bigint?

我的建议

所有现有的 Web API 都只返回和接受数字,并且只会根据具体情况升级到 bigint。

18.9.2 为什么不像 bigint 那样简单地提高数字的精度?

可以想象将 number 拆分为 integerdouble,但这会给语言增加许多新的复杂性(几个仅限整数的运算符等)。我在 Gist 中概述了后果。


致谢