我正在参加「掘金·启航计划」
JavaScript BigInt 使用指南
我们都知道 JavaScript 是一门不断发展的语言。在 ES2020 的 2020 版发布之前,没有原生方法可以准确表示和执行大于 9,007,199,254,740,991
或 2**53 – 1
或小于 -9,007,199,254,740,991
或 -(2**53 – 1)
的数字的数学运算。大多数开发人员依赖 JSBI 和 bignumber.js 等库来对非常大的数字执行计算。
这种情况产生了另一种数据类型,称为 BigInt
。 BigInt
的引入使 JavaScript 数据类型总数达到了八种。在本文中,我们将了解 BigInt
是什么、它的优点以及如何在 JavaScript 中正确使用它。要了解有关在 Node.js 应用程序中表示大数的更多信息,请查看 如何在 Node.js 应用程序中处理大数。
什么是 BigInt?
BigInt
是一种数值数据类型,可用于表示旧数值数据类型 number
无法表示的小数和大数。每个 BigInt
值都必须在数字后包含小写字母 n
,例如 897n
。因此,说 21
不严格等于 21n
是准确的,因为前者是一个 number
,而后者是一个 bigint
。
当你对任何大于 9,007,199,254,740,991
或 Number.MAX_SAFE_INTEGER
的数字执行数学运算时,你实际上并没有得到准确的答案。我们可以看到下面的示例:
const largeNumber = Number.MAX_SAFE_INTEGER; // or 9007199254740991
const addOne = largeNumber + 1;
console.log(addOne);
// returns 9007199254740992
const addTwo = largeNumber + 2;
console.log(addTwo);
// Also returns 9007199254740992
// Should return 9007199254740993
const addSix = largeNumber + 6;
console.log(addSix);
// returns 9007199254740996
// Should return 9007199254740997
const bb = 12n;
console.log(bb);
但是,我们可以使用 BigInt
来准确地处理这些类型的操作。要将整数定义为 BigInt
,我们可以执行以下任一方法:我们可以将 n
附加到整数或对整个数值或仅包含整数值的字符串调用 BigInt()
构造函数。例如下面的示例:
// Method 1
const sampleBigIntOne = 234n;
// Method 2
const sampleBigIntTwo = BigInt(567)
// returns 567n
const sampleBigIntThree = BigInt("123")
// returns 123n
const sampleBigIntFour = 12356789900099999999912111n
// 仍然正确
要验证一个值是 BigInt
类型,我们可以像这样使用 typeof
运算符:
const sampleOne = 17n
console.log(typeof sampleOne)
// Expected result: "bigint"
const sampleTwo = BigInt(789);
console.log(typeof sampleTwo)
// Expected result: "bigint"
// 这是一个不推荐的写法
const sampleThree = typeof 17n === "bigint"
console.log(sampleThree)
// Expected result: true
注意:在 BigInt
构造函数中传递数字是一种不推荐的写法法。相反,最好只附加 n
或首先将其包装在一个字符串中,分别如 sampleBigIntOne
和 sampleBigIntThree
中那样。
BigInt 的使用示例
BigInt
值在大数至为重要的生活或计算领域得到了应用。以下是 BigInt
的一些关键用例:
- 财务计算:
BigInt
在财务计算中很重要,因为可以处理非常大量的交易和货币转换 - 密码学和安全计算:
BigInt
在密码学中用于生成非常难以预测或破解的非常大的随机数 - 游戏开发:在游戏开发中,大数通常用于保存时间戳、分数和跟踪进度。
BigInt
的使用确保了此类值的准确表示 - 分布式系统:分布式系统需要唯一身份才能准确执行。由于
BigInt
值是无限的,因此它们可用于生成标识符和密钥
BigInt
在这些领域确实很重要,因为它让开发人员安全、精确地处理巨大的整数、时间戳、ID 和进程。
BigInt vs Number
通常,JavaScript 中的数字可以用来表示整数,甚至可以表示浮点数,比如 69
和 4.125677
。它包括介于 9,007,199,254,740,991
或 2**53 – 1
和 -9,007,199,254,740,991
或 -(2**53 – 1)
之间的值。这些限制可用作常量,例如分别为 Number.MAX_SAFE_INTEGER
和 Number.MIN_SAFE_INTEGER
。
实际上,你可以对数字进行所有算术运算,例如加法 (+
)、减法 (-
)、乘法 (*
)、除法 (/
)、取余或取模 (%
) 和指数 (**
)在该范围内没有任何问题,但 BigInt
并非完全如此。
BigInt
可用于表示和执行任何大小的整数运算,浮点数或小数除外,包括大于 9,007,199,254,740,991
或小于 -9,007,199,254,740,991
的数字。例如 17.2n
这样的十进制值是无效的。
通常,在两个或多个 BigInt
值之间使用时,除 (/
) 之外的所有算术运算都会给出正确的数学答案。除法运算符不会始终给出准确的结果,所以 5n/2n
等运算将舍弃小数部分为 2n
而不是 2.5n
。
Number 类型的限制
JavaScript 数字类型被写成双精度 64 位二进制格式 IEEE 754 值,52 位用于有效值,而 1 位和 11 位用于符号和指数。因此,无论何时使用 Number
,我们都可能面临一些限制。这些限制包括:
- 舍入误差和数字精度有限
- 无法在最小和最大安全整数之外执行精确运算
- 无法执行超出
MAX_VALUE
和MIN_VALUE
范围的操作
JavaScript 必然会对数字进行四舍五入,因为它使用 64 位来表示浮点数。这意味着 JavaScript 中的每个数字在存储到内存中之前都会先转换为双精度二进制浮点数。而数字的 64 位表示实际上分为三部分,包括有效数字、偏移指数和符号。
例如,像 15 这样的数字转换后如下所示: 0.10000000010.1110000000000000000000000000000000000000000000000000
。
双精度二进制浮点数的第一部分是符号,可以是 0 或 1,代表数字的正负号。因此,符号为 0 时为正,反之亦然。占用 1 位。第二部分是偏移指数,占用 11 位,最后一部分是有效数字或尾数,占用 52 位。
有些数字给出精确值,例如 1/2
、1/4
、1/5
、1/8
、1/10
以及 9,007,199,254,740,991
或 2**53 – 1
到 -9,007,199,254,740,991
或 -(2**53 – 1)
。其他一些数字没有给出确切的值;相反,它们是重复小数,例如 1/3
、1/6
、1/7
、1/9
。
问题在于,其中一些十进制数在转换为二进制时会失去精度,反之亦然。因此,当你取一个小数(例如 0.1
)并将其加起来为 0.2
时,它们加起来不会等于 0.3
。相反,它们加起来为 0.30000000000000004
。更多示例如下:
const sample1 = 0.2 + 0.6;
console.log(sample1); // 0.6000000000000001
const sample2 = 0.3 + 0.6;
console.log(sample2); // 0.8999999999999999
当使用数字时,这是一个巨大的问题,因为它会给需要高精度的计算带来很大的风险。当我们使用 BigInt
时,这个问题就可以避免,因为它不接受小数。
仅当你执行 9,007,199,254,740,991
或 2**53 – 1
到 -9,007,199,254,740,991
或 -(2**53 – 1)
之间的算术运算时,JavaScript 数字才能精确。所以你在该范围之外执行的任何操作都可能会导致错误的答案,例如:
const sample3 = 9_007_199_254_740_991 + 1; // 9,007,199,254,740,992
const sample4 = 9_007_199_254_740_991 + 2; // 9,007,199,254,740,992
console.log(sample3 === sample4); // true
JavaScript 对它可以表示的数字大小也有限制。它们可以用两个内置常量来表示:Number.MAX_VALUE
和 Number.MIN_VALUE
。它们本质上是数字:分别为 1.7976931348623157e+308
和 5e-324
。
每当你尝试使用 Number.MAX_VALUE
执行加法或减法时,它都会返回 1.7976931348623157e+308
。当你尝试对 Number.MIN_VALUE
执行相同操作时,它会返回以下示例中的值:
const sample5 = Number.MAX_VALUE + 7;
console.log(sample5); // 1.7976931348623157e+308
const sample6 = Number.MAX_VALUE - 23;
console.log(sample6); // 1.7976931348623157e+308
const sample7 = Number.MIN_VALUE + 5;
console.log(sample7); // 5
const sample8 = Number.MIN_VALUE - 54;
console.log(sample8); // -54
当你对 Number.MAX_VALUE
执行乘法 (*
) 或指数 (**
) 时,它总是返回无穷大。该值表示为 Number.POSITIVE_INFINITY
。
只有除法适用于 Number.MAX_VALUE
,只有乘法适用于 Number.MIN_VALUE
。当你需要执行涉及非常大的数字的计算时,这是一个严重的限制。然而,在 BigInt
的帮助下,我们可以对非常大或非常小的数字进行计算。
使用 BigInt 的优点
现在,让我们深入探讨在 JavaScript 中使用 BigInt
的优势。首先,BigInt
确保我们不会遇到精度错误。当我们处理数字时,我们可以使用小数,这可能会导致精度和舍入误差,正如我们在上一节中看到的那样。
但是,当我们使用 BigInt
时,我们不允许使用小数。这可确保始终避免此类错误。因此,执行以下操作是无效的:
const sample1 = 5.4n;
// Uncaught SyntaxError: Invalid or unexpected token
BigInt
还可以执行任意精度的计算。与在 Number.MAX_SAFE_INTEGER
和 Number.MIN_SAFE_INTEGER
范围之外执行计算时丢失精度的数字不同,BigInt
允许我们在不丢失任何精度的情况下执行此类计算。如下例所示:
const sample2 = BigInt(Number.MAX_SAFE_INTEGER) + 1n
const sample3 = BigInt(Number.MAX_SAFE_INTEGER) + 2n
console.log(sample2); // 9007199254740992n
console.log(sample3); // 9007199254740993n
console.log(sample3 > sample2); // true
而且使用 BigInt
,我们可以安全、精确地对大于 Number.MAX_VALUE
的整数进行计算,因为它具有任意精度,所以它可以执行操作的整数的大小仅受主机上可用内存的限制计算机或系统。我们在使用 BigInt
时不会遇到同样的问题:
const max = BigInt(Number.MAX_VALUE);
console.log(max);
// 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368n
const sample4 = max + 7;
// 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858375n
const sample5 = max - 23;
// 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858345n
此外,BigInt
允许我们为计算的位数做出限制(设置边界),从而我们可以为程序定义适当的整数范围。我们可以使用 BigInt
原型上提供的 asIntN
和 asUintN
方法来实现这一点。
本质上,这两个方法分别用于将 BigInt
值包装为宽度位固定的二进制有符号和无符号整数。有符号整数用于表示负值,反之亦然。
因此,如果我们想将计算限制为 8 位、16 位、32 位或 64 位算术,我们可以使用 asIntN
方法通过 BigInt.asIntN(width, value)
调用它来实现。在这种情况下,width
参数表示所需的大于零的位,而 value
参数表示我们想要限制在提供的width
内的 BigInt
值。
让我们看一个例子:
const maximumBits = 16;
const valueOne = BigInt(32767);
const constrainedValueOne = BigInt.asIntN(maximumBits, valueOne);
console.log(constrainedValueOne); // 32767n
const valueTwo = BigInt(32768);
const constrainedValueTwo = BigInt.asIntN(maximumBits, valueTwo);
console.log(constrainedValueTwo); // -32768n
正如你在上面的示例中看到的,我们通过使用 asIntN
创建了要使用的 16 位整数限制。一般来说,16 位只能包含 -32768n
到 32767n
之间的整数。因此,在第一个示例中,它按原样返回值,而在后者中,它截断了该值,因为它超出了我们指定的范围。
BigInt
还提高了 JavaScript 应用程序的速度。在 ES2020 之前,开发人员依赖 Math.js 和 JSBI 等库来计算大数。然而,其中一些包又大又慢。这导致使用 JavaScript 构建的应用程序和软件执行变慢;然而,BigInt
的 出现可以改进执行大量计算的应用程序。
如何在 JavaScript 中使用 BigInt
由于 BigInt
是一种数据类型,因此它与 Number
类似,也可用于执行计算。要定义 BigInt
值,我们可以使用 BigInt
标识(即 n
),或者对包含整数或数字的字符串使用 BigInt
构造函数。
但是,我们应该警惕使用构造函数将数字强制转换为 BigInt
,因为即使在转换发生之前数字也可能会丢失精度。例如:
const dontDoThis = BigInt(12345567891234567890)
console.log(dontDoThis); // 12345567891234568192n
const dontDoThisToo = BigInt(`${12345567891234567890}`)
console.log(dontDoThis); // 12345567891234568000n
使用 BigInt 的方法
你可以调用 BigInt
类的五种不同的方法:
BigInt.prototype.toString()
BigInt.prototype.valueOf()
BigInt.prototype.toLocaleString()
BigInt.asIntN()
BigInt.asUintN()
每当遇到像 BigInt.prototype.toString()
这样的方法时,这意味着 toString()
方法将在 BigInt
对象实例(如 5n.toString()
)上调用,而诸如 BigInt.asUintN()
之类的方法将直接在 BigInt
构造函数上调用 BigInt.asIntN(maximumBits, valueTwo)
。
BigInt.prototype.toString()
首先,让我们看一下 BigInt.prototype.toString()
。 toString()
用于将 BigInt
值转换为字符串。它本质上是用双引号或单引号包裹 BigInt
,同时删除尾随的 n
。它可以这样使用:
const value1 = 35n;
const sample1 = value1.toString();
console.log(sample1); // "35"
你可以使用 typeof
验证返回值现在是否为字符串,如下所示:
const value1 = 35n;
const sample1 = value1.toString();
const valueDataType = typeof sample1;
console.log(valueDataType); // "string"
或者,你还可以在将 BigInt
转换为字符串时传递基数。基数实际上是你要转换为的基数。要传递的基数范围为 2 到 36。此外,当没有传递基数时,它默认为基数 10。因此,你可以传递这样的基数:
const value = 10n;
const newValueBase16 = value.toString(16);
console.log(newValueBase16); // "a"
const newValueBase5 = value.toString(5);
console.log(newValueBase5); // "20"
const newValueBase10 = value.toString(10);
console.log(newValueBase10);// "10"
BigInt.prototype.valueOf()
valueOf
方法用于获取 BigInt
对象的原始类型。看下面的例子:
const value = Object(7n);
const valueOfDataType1 = typeof Object(7n);
console.log(valueOfDataType1); // "object"
// 你可以使用 .valueOf 方法来获取原始类型
const valueOfDataType2 = typeof Object(7n).valueOf();
console.log(valueOfDataType2); // "bigint"
BigInt.prototype.toLocaleString()
toLocaleString()
方法与 toString()
类似,但是,它可用于以特定于语言的方式或格式返回字符串。它接受两个参数,即 locale
和 options
。它可以这样使用:
const value = 23345689n;
const valueAsFormattedString = value.toLocaleString('en-US');
console.log(valueAsFormattedString); // 23,345,689
const valueAsFormattedStringWithOptions = value.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
console.log(valueAsFormattedStringWithOptions); // $23,345,689.00
BigInt.asIntN()
BigInt.asIntN()
用于将 BigInt
值限制在设定的位宽内,同时保留值的符号。语法:BigInt.asIntN(bitWidth, BigIntValue)
。因此,当我们想要保留 BigInt
值的符号(即使我们想要约束它)时,它是合适的。
一般来说,bitWidth
可以是 8 位、16 位、32 位等。因此,当我们将宽度设置为 8 位时,实质上是说我们要接受总共 256 个 BigInt 值,范围为 -128
到 128
之间。如果该值落在该位所容纳值的范围内,则按原样返回该值;如果超出该范围,则返回与该位中的值相当的值。例如:
const bits = 8;
const value1 = 126n; // 在范围为 -128 到 128 之间
const value2 = 127n; // 在范围为 -128 到 128 之间
const value3 = 128n; // 不在范围为 -128 到 128 之间
const value4 = 129n; // 不在范围为 -128 到 128 之间
const value5 = -67n; // 在范围为 -128 到 128 之间
const result1 = BigInt.asIntN(bits, value1);
console.log(result1); //126n
const result2 = BigInt.asIntN(bits, value2);
console.log(result2); // 127n
const result3 = BigInt.asIntN(bits, value3);
console.log(result3); // -128n
const result4 = BigInt.asIntN(bits, value4);
console.log(result4); // -127n
const result5 = BigInt.asIntN(bits, value5);
console.log(result5); // -67n
BigInt.asUintN()
BigInt.asUintN()
与 BigInt.asIntN()
类似。主要区别在于,它忽略要约束的 BigInt
值上的符号,该符号仅约束在零和位可以包含的最大数量之间。例如,8 位可以包含 256 个值;因此,它将限制在 0 到 256 之间,其中包括 0。例如:
const bits = 8;
const value1 = 254n; // 在 0 到 256 之间
const value2 = 255n; // 在 0 到 256 之间
const value3 = 256n; // 不在 0 到 256 之间
const value4 = 257n; // 不在 0 到 256 之间
const value5 = 258n; // 不在 0 到 256 之间
const result1 = BigInt.asUintN(bits, value1);
console.log(result1); // 254n
const result2 = BigInt.asUintN(bits, value2);
console.log(result2); // 255n
const result3 = BigInt.asUintN(bits, value3);
console.log(result3); // 0n
const result4 = BigInt.asUintN(bits, value4);
console.log(result4); // 1n
const result5 = BigInt.asUintN(bits, value5);
console.log(result5); // 2n
BigInt 条件语句
适用于 Number
的条件也适用于 BigInt
。这意味着 ||
、&&
和 !
也可以与 BigInt
正常操作。另外,当我们使用 if
语句时,只有值 0n
才会计算为 false
,而其他值则为 true
:
if(0n){
console.log('It is in if')
}else{
console.log("it is in else")
}
// "it is in else"
if(-2n){
console.log("it is in if")
}else{
console.log("it is in else")
}
// "it is in if"
if(67n){
console.log("it is in if")
}else{
console.log("it is in else")
}
// "it is in if"
即使是 Boolean
函数也能按预期运行,只有 0n
才会计算为 false
,如下所示:
const isValueNone = Boolean(0n);
console.log(isValueNone); // false
const isValueExists1 = Boolean(79n);
console.log(isValueExists1); // true
const isValueExists2 = Boolean(-65n);
console.log(isValueExists2); // true
BigInt 常见规则及注意事项
为了使用 BigInt
而不会出现错误,我们需要遵循一些规则:
- 创建
BigInt
时不要使用new
关键字 - 不要在
Bigint
中使用小数 BigInt
强制转换错误- 不要直接将
JSON.stringify
与BigInt
一起使用 - 内置方法有限
BigInt
的除法运算返回截断的结果
首先,创建 BigInt
时不要使用 new
关键字。在 JavaScript 中,我们经常使用 new
关键字来初始化类的实例。然而,BigInt
的情况并非如此,因为当我们使用 new
关键字时,它会抛出 TypeError
:
const value = new BigInt(54);
// TypeError: BigInt is not a constructor
另外,请勿在 Bigint
中使用小数。当我们尝试将小数转换为 BigInt
值时,它会抛出 RangeError
。类似地,当使用带有小数点的隐式转换的 BigInt
时,我们将得到一个 SyntaxError
:
const value = 7.8n + 9n;
// SyntaxError: Invalid or unexpected token
此外,你可能会遇到 BigInt
强制转换的错误,例如将 Number
与 BigInt
混合将引发 TypeError
:
const result = 2n + 4;
console.log(result);
// TypeError: Cannot mix BigInt and other types, use explicit conversions
并且,将 null
、undefined
和 Symbol
与 BigInt
一起使用将抛出错误:
console.log(BigInt(null));
// TypeError: can't convert null to BigInt
console.log(BigInt(undefined));
// TypeError: can't convert undefined to BigInt
console.log(BigInt(Symbol()));
// TypeError: can't convert Symbol() to BigInt
为了避免更多错误,请勿直接将 JSON.stringify
与 BigInt
一起使用。我们不能直接在 BigInt
上使用 JSON.stringify
会抛出 TypeError
:
const theStringified = JSON.stringify(5n);
console.log(theStringified);
// TypeError: BigInt value can't be serialized in JSON
相反,我们可以将 BigInt
值字符串化以实现 toJSON
方法或者实现一个 replacer
方法。要实现 toJSON
方法,我们只需在使用 JSON.stringify()
方法之前将以下代码添加到程序文件中:
BigInt.prototype.toJSON = function () {
return this.toString();
};
要使用 replacer
方法实现 JSON.stringify()
方法,我们只需将以下代码添加到我们的程序中:
const replacer = (key, value) => {
return typeof value === "bigint" ? value.toString() : value
}
我们可以使用以下两种方法中的任何一种来实现 JSON.stringify()
:
// 方法一
// 使用 toJSON
// 把这个放在使用 JSON.stringify() 的地方之前
BigInt.prototype.toJSON = function () {
return this.toString();
};
const value1 = {one: 5n, two: 667n};
const value1ToJson = JSON.stringify(value1);
console.log(value1ToJson);
// {"one":"5","two":"667"}
// 方法二
// 使用 replacer
const replacer = (key, value) => {
return typeof value === "bigint" ? value.toString() : value
}
const value2 = {seven: 7n, twenty: 20n, five: 5n};
const value2ToJson = JSON.stringify(value2, replacer);
console.log(value2ToJson);
// {"seven":"7","twenty":"20","five":"5"}
BigInt 的局限性
BigInt
的限制之一是我们无法在 JavaScript 中使用内置的 Math
函数。相反,我们应该自己构建数学函数。因此,执行诸如 Math.sqrt()
之类的操作将抛出 TypeError
异常:
console.log(Math.sqrt(4n))
// TypeError: can't convert BigInt to number
此外,使用 BigInt
进行除法运算会返回截断的结果。因为 BigInt
值不能是小数,而除法运算,有时会返回小数被除数,例如 7 / 4 = 1.75
。因此,BigInt
值将始终舍入为 0
。这意味着 7n / 4n = 1n
而不是预期的 1.75n
。
浏览器和 Node.js 的支持
自 v10.4
起的任何 Node.js 版本(包括 v18.16
等较新版本)都支持 BigInt
,所以旧版本将抛出语法错误。同样,大多数现代浏览器都支持 BigInt
。你可以在 caniuse 上查看详情。
截至撰写本文时,超过 94% 的浏览器支持 BigInt
构造函数及其方法。
小结
在本文中,我们了解了 BigInt
、它的可用方法、用例以及使用它的问题。然而,我们应该知道,Number
对于小型网站等日常程序也很有用,并且只有当我们确定要处理大数时,才应该使用 BigInt
。
谢谢阅读。我希望你喜欢这篇文章,如果你有任何疑问,请务必发表评论。