_
) 分隔符 [ES2021]+
) 和一元减号 (-
)++
) 和递减 (--
)NaN
Infinity
b32()
:以二进制表示形式显示无符号 32 位整数Number
的静态属性Number
的静态方法Number.prototype
的方法JavaScript 有两种数值类型
本章介绍数字。BigInt 在本书后面介绍。
在 JavaScript 中,number
类型用于表示整数和浮点数
98
123.45
但是,所有数字都是双精度的,即根据IEEE 浮点算术标准 (IEEE 754) 实现的 64 位浮点数。
整数只是没有小数部分的浮点数
> 98 === 98.0true
请注意,在底层,大多数 JavaScript 引擎通常能够使用真正的整数,并具有所有相关的性能和存储大小优势。
让我们来看看数字的字面量。
几种整数字面量允许我们用不同的进制表示整数
// Binary (base 2)
.equal(0b11, 3); // ES6
assert
// Octal (base 8)
.equal(0o10, 8); // ES6
assert
// Decimal (base 10)
.equal(35, 35);
assert
// Hexadecimal (base 16)
.equal(0xE7, 231); assert
浮点数只能用十进制表示。
分数
> 35.035
指数:eN
表示 ×10N
> 3e2300
> 3e-20.03
> 0.3e230
访问整数字面量的属性会导致一个陷阱:如果整数字面量后面紧跟着一个点,那么该点会被解释为小数点
7.toString(); // syntax error
有四种方法可以解决这个陷阱
7.0.toString()
7).toString()
(7..toString()
7 .toString() // space before dot
_
) 分隔符 [ES2021]对数字进行分组以提高长数字的可读性由来已久。例如
从 ES2021 开始,我们可以在数字字面量中使用下划线作为分隔符
const inhabitantsOfLondon = 1_335_000;
const distanceEarthSunInKm = 149_600_000;
对于其他进制,分组也很重要
const fileSystemPermission = 0b111_111_000;
const bytes = 0b1111_10101011_11110000_00001101;
const words = 0xFAB_F00D;
我们也可以在分数和指数中使用分隔符
const massOfElectronInKg = 9.109_383_56e-31;
const trillionInShortScale = 1e1_2;
分隔符的位置受到两种方式的限制
我们只能在下划线两边放置数字。因此,以下所有数字字面量都是非法的
3_.141
3._141
1_e12
1e_12
// valid variable name!
_1464301 1464301_
0_b111111000
0b_111111000
我们不能连续使用多个下划线
123__456 // two underscores – not allowed
这些限制背后的动机是保持解析简单并避免奇怪的边缘情况。
以下用于解析数字的函数不支持分隔符
Number()
Number.parseInt()
Number.parseFloat()
例如
> Number('123_456')NaN
> Number.parseInt('123_456')123
其理由是数字分隔符是用于代码的。其他类型的输入应该以不同的方式处理。
表 5 列出了 JavaScript 的二元算术运算符。
运算符 | 名称 | 示例 | |
---|---|---|---|
n + m |
加法 | ES1 | 3 + 4 → 7 |
n - m |
减法 | ES1 | 9 - 1 → 8 |
n * m |
乘法 | ES1 | 3 * 2.25 → 6.75 |
n / m |
除法 | ES1 | 5.625 / 5 → 1.125 |
n % m |
取余 | ES1 | 8 % 5 → 3 |
-8 % 5 → -3 |
|||
n ** m |
幂运算 | ES2016 | 4 ** 2 → 16 |
%
是取余运算符%
是取余运算符,而不是取模运算符。其结果的符号与第一个操作数相同
> 5 % 32
> -5 % 3-2
有关取余和取模之间区别的更多信息,请参阅 2ality 上的博客文章 “取余运算符与取模运算符(使用 JavaScript 代码)”。
+
) 和一元减号 (-
)表 6 总结了两个运算符:一元加号 (+
) 和一元减号 (-
)。
运算符 | 名称 | 示例 | |
---|---|---|---|
+n |
一元加号 | ES1 | +(-7) → -7 |
-n |
一元减号 | ES1 | -(-7) → 7 |
这两个运算符都会将其操作数强制转换为数字
> +'5'5
> +'-12'-12
> -'9'-9
因此,一元加号允许我们将任意值转换为数字。
++
) 和递减 (--
)递增运算符 ++
存在前缀版本和后缀版本。在这两个版本中,它都会破坏性地将其操作数加 1。因此,其操作数必须是可以更改的存储位置。
递减运算符 --
的工作原理相同,但会将其操作数减 1。接下来的两个示例解释了前缀版本和后缀版本之间的区别。
表 7 总结了递增和递减运算符。
运算符 | 名称 | 示例 | |
---|---|---|---|
v++ |
递增 | ES1 | let v=0; [v++, v] → [0, 1] |
++v |
递增 | ES1 | let v=0; [++v, v] → [1, 1] |
v-- |
递减 | ES1 | let v=1; [v--, v] → [1, 0] |
--v |
递减 | ES1 | let v=1; [--v, v] → [0, 0] |
接下来,我们将查看这些运算符的使用示例。
前缀 ++
和前缀 --
会先更改其操作数,然后返回更改后的值。
let foo = 3;
.equal(++foo, 4);
assert.equal(foo, 4);
assert
let bar = 3;
.equal(--bar, 2);
assert.equal(bar, 2); assert
后缀 ++
和后缀 --
会先返回其操作数,然后更改其值。
let foo = 3;
.equal(foo++, 3);
assert.equal(foo, 4);
assert
let bar = 3;
.equal(bar--, 3);
assert.equal(bar, 2); assert
我们也可以将这些运算符应用于属性值
const obj = { a: 1 };
++obj.a;
.equal(obj.a, 2); assert
以及数组元素
const arr = [ 4 ];
0]++;
arr[.deepEqual(arr, [5]); assert
练习:数字运算符
exercises/numbers-math/is_odd_test.mjs
以下是将值转换为数字的三种方法
Number(value)
+value
parseFloat(value)
(避免使用;与其他两种方法不同!)建议:使用描述性的 Number()
。表 8 总结了它的工作原理。
x |
Number(x) |
---|---|
undefined |
NaN |
null |
0 |
boolean | false → 0 , true → 1 |
number | x (无变化) |
bigint | -1n → -1 , 1n → 1 ,等等。 |
string | '' → 0 |
其他 → 解析后的数字,忽略前导/尾随空格 |
|
symbol | 抛出 TypeError |
object | 可配置(例如,通过 .valueOf() ) |
示例
.equal(Number(123.45), 123.45);
assert
.equal(Number(''), 0);
assert.equal(Number('\n 123.45 \t'), 123.45);
assert.equal(Number('xyz'), NaN);
assert
.equal(Number(-123n), -123); assert
如何将对象转换为数字是可配置的,例如,通过覆盖 .valueOf()
> Number({ valueOf() { return 123 } })123
练习:转换为数字
exercises/numbers-math/parse_number_test.mjs
发生错误时会返回两个数字值
NaN
Infinity
NaN
NaN
是“非数字”的缩写。讽刺的是,JavaScript 将其视为数字
> typeof NaN'number'
什么时候会返回 NaN
?
如果无法解析数字,则返回 NaN
> Number('$$$')NaN
> Number(undefined)NaN
如果无法执行操作,则返回 NaN
> Math.log(-1)NaN
> Math.sqrt(-1)NaN
如果操作数或参数是 NaN
,则返回 NaN
(以传播错误)
> NaN - 3NaN
> 7 ** NaNNaN
NaN
NaN
是唯一一个不严格等于自身的 JavaScript 值
const n = NaN;
.equal(n === n, false); assert
以下几种方法可以检查值 x
是否为 NaN
const x = NaN;
.equal(Number.isNaN(x), true); // preferred
assert.equal(Object.is(x, NaN), true);
assert.equal(x !== x, true); assert
在最后一行中,我们使用比较怪癖来检测 NaN
。
NaN
某些数组方法无法找到 NaN
> [NaN].indexOf(NaN)-1
其他方法可以
> [NaN].includes(NaN)true
> [NaN].findIndex(x => Number.isNaN(x))0
> [NaN].find(x => Number.isNaN(x))NaN
遗憾的是,没有简单的经验法则。我们必须针对每种方法检查它如何处理 NaN
。
Infinity
什么时候会返回错误值 Infinity
?
如果数字太大,则返回 Infinity
> Math.pow(2, 1023)8.98846567431158e+307
> Math.pow(2, 1024)Infinity
如果除以零,则返回 Infinity
> 5 / 0Infinity
> -5 / 0-Infinity
Infinity
作为默认值Infinity
比所有其他数字都大(除了 NaN
),这使其成为一个很好的默认值
function findMinimum(numbers) {
let min = Infinity;
for (const n of numbers) {
if (n < min) min = n;
}return min;
}
.equal(findMinimum([5, -1, 2]), -1);
assert.equal(findMinimum([]), Infinity); assert
Infinity
以下两种常见方法可以检查值 x
是否为 Infinity
const x = Infinity;
.equal(x === Infinity, true);
assert.equal(Number.isFinite(x), false); assert
练习:比较数字
exercises/numbers-math/find_max_test.mjs
在内部,JavaScript 浮点数使用二进制表示(根据 IEEE 754 标准)。这意味着十进制小数并不总是能够精确表示
> 0.1 + 0.20.30000000000000004
> 1.3 * 33.9000000000000004
> 1.4 * 100000000000000139999999999999.98
因此,在 JavaScript 中执行算术运算时,我们需要考虑舍入误差。
继续阅读以了解此现象的解释。
测验:基础
请参阅测验应用程序。
本章的其余部分均为进阶内容。
在 JavaScript 中,使用数字进行计算并不总是产生正确的结果,例如
> 0.1 + 0.20.30000000000000004
要理解原因,我们需要了解 JavaScript 如何在内部表示浮点数。它使用三个整数来表示,总共占用 64 位存储空间(双精度)
组成部分 | 大小 | 整数范围 |
---|---|---|
符号 | 1 位 | [0, 1] |
分数 | 52 位 | [0, 252−1] |
指数 | 11 位 | [−1023, 1024] |
由这些整数表示的浮点数的计算方法如下:
(–1)符号位 × 0b1.小数部分 × 2指数
这种表示方法无法编码零,因为它的第二部分(涉及小数部分)始终以 1 开头。因此,零是通过特殊指数 -1023 和小数部分 0 来编码的。
为了使进一步的讨论更容易,我们简化了之前的表示方法。
新的表示方法如下:
尾数 × 10指数
让我们用这种表示方法来表示几个浮点数。
对于整数 -123,我们主要需要尾数。
> -123 * (10 ** 0)-123
对于数字 1.5,我们假设尾数后面有一个小数点。我们使用负指数将该小数点向左移动一位。
> 15 * (10 ** -1)1.5
对于数字 0.25,我们将小数点向左移动两位。
> 25 * (10 ** -2)0.25
具有负指数的表示形式也可以写成分母中具有正指数的分数。
> 15 * (10 ** -1) === 15 / (10 ** 1)true
> 25 * (10 ** -2) === 25 / (10 ** 2)true
这些分数有助于理解为什么有些数字我们的编码无法表示。
1/10
可以表示。它已经具有所需的格式:分母中是 10 的幂。
1/2
可以表示为 5/10
。我们通过将分子和分母都乘以 5,将分母中的 2 变为 10 的幂。
1/4
可以表示为 25/100
。我们通过将分子和分母都乘以 25,将分母中的 4 变为 10 的幂。
1/3
无法表示。没有办法将分母变为 10 的幂。(10 的质因数是 2 和 5。因此,任何只有这些质因数的分母都可以通过将分子和分母都乘以足够多的 2 和 5 来转换为 10 的幂。如果分母有不同的质因数,那么我们就无能为力了。)
为了结束我们的探索,我们回到以 2 为底。
0.5 = 1/2
可以用以 2 为底表示,因为分母已经是 2 的幂。0.25 = 1/4
可以用以 2 为底表示,因为分母已经是 2 的幂。0.1 = 1/10
无法表示,因为分母无法转换为 2 的幂。0.2 = 2/10
无法表示,因为分母无法转换为 2 的幂。现在我们可以看到为什么 0.1 + 0.2
不会产生正确的结果:在内部,这两个操作数都不能精确表示。
使用十进制小数进行精确计算的唯一方法是在内部切换到以 10 为底。对于许多编程语言来说,以 2 为底是默认的,以 10 为底是一个选项。例如,Java 有 BigDecimal
类,Python 有 decimal
模块。JavaScript 也计划添加类似的功能:ECMAScript 提案“Decimal”。
整数是没有小数部分的普通(浮点)数。
> 1 === 1.0true
> Number.isInteger(1.0)true
在本节中,我们将介绍一些用于处理这些伪整数的工具。JavaScript 还支持大整数,它们是真正的整数。
将数字转换为整数的推荐方法是使用 Math
对象的舍入方法之一。
Math.floor(n)
:返回最大的整数 i
≤ n
。
> Math.floor(2.1)2
> Math.floor(2.9)2
Math.ceil(n)
:返回最小的整数 i
≥ n
。
> Math.ceil(2.1)3
> Math.ceil(2.9)3
Math.round(n)
:返回“最接近” n
的整数,其中 __.5
向上舍入,例如:
> Math.round(2.4)2
> Math.round(2.5)3
Math.trunc(n)
:删除 n
的任何小数部分(小数点后),从而将其转换为整数。
> Math.trunc(2.1)2
> Math.trunc(2.9)2
有关舍入的更多信息,请参阅§17.3 “舍入”。
以下是 JavaScript 中重要的整数范围:
>>>
) 的范围:无符号,[0, 232)这是 JavaScript 中安全的整数范围(53 位加符号位)。
[–(253)+1, 253–1]
如果一个整数由一个 JavaScript 数字精确表示,则它是安全的。鉴于 JavaScript 数字被编码为一个分数乘以 2 的指数幂,因此也可以表示更大的整数,但它们之间存在间隙。
例如(18014398509481984 是 254):
> 1801439850948198418014398509481984
> 1801439850948198518014398509481984
> 1801439850948198618014398509481984
> 1801439850948198718014398509481988
Number
的以下属性有助于确定一个整数是否安全:
.equal(Number.MAX_SAFE_INTEGER, (2 ** 53) - 1);
assert.equal(Number.MIN_SAFE_INTEGER, -Number.MAX_SAFE_INTEGER);
assert
.equal(Number.isSafeInteger(5), true);
assert.equal(Number.isSafeInteger('5'), false);
assert.equal(Number.isSafeInteger(5.1), false);
assert.equal(Number.isSafeInteger(Number.MAX_SAFE_INTEGER), true);
assert.equal(Number.isSafeInteger(Number.MAX_SAFE_INTEGER+1), false); assert
练习:检测安全整数
exercises/numbers-math/is_safe_integer_test.mjs
让我们看看涉及不安全整数的计算。
以下结果是不正确且不安全的,即使它的两个操作数都是安全的:
> 9007199254740990 + 39007199254740992
以下结果是安全的,但不正确。第一个操作数是不安全的;第二个操作数是安全的:
> 9007199254740995 - 109007199254740986
因此,表达式 a op b
的结果正确,当且仅当:
isSafeInteger(a) && isSafeInteger(b) && isSafeInteger(a op b)
也就是说,两个操作数和结果都必须是安全的。
在内部,JavaScript 的按位运算符使用 32 位整数。它们按以下步骤生成结果:
对于每个按位运算符,本书都提到了其操作数和结果的类型。每种类型始终是以下两种之一:
类型 | 描述 | 大小 | 范围 |
---|---|---|---|
Int32 | 有符号 32 位整数 | 32 位(包括符号位) | [−231, 231) |
Uint32 | 无符号 32 位整数 | 32 位 | [0, 232) |
考虑到前面提到的步骤,我建议假装按位运算符在内部使用无符号 32 位整数(“计算”步骤),而 Int32 和 Uint32 只影响 JavaScript 数字如何转换为整数以及从整数转换(“输入”和“输出”步骤)。
在探索按位运算符时,偶尔以二进制表示法显示无符号 32 位整数形式的 JavaScript 数字会有所帮助。这就是 b32()
的作用(其实现将在后面展示)。
.equal(
assertb32(-1),
'11111111111111111111111111111111');
.equal(
assertb32(1),
'00000000000000000000000000000001');
.equal(
assertb32(2 ** 31),
'10000000000000000000000000000000');
操作 | 名称 | 类型签名 | |
---|---|---|---|
~num |
按位非,反码 | Int32 → Int32 |
ES1 |
按位非运算符(表9)反转其操作数的每个二进制位。
> b32(~0b100)'11111111111111111111111111111011'
这种所谓的反码对于某些算术运算类似于负数。例如,将一个整数与其反码相加始终为 -1
。
> 4 + ~4-1
> -11 + ~-11-1
操作 | 名称 | 类型签名 | |
---|---|---|---|
num1 & num2 |
按位与 | Int32 × Int32 → Int32 |
ES1 |
num1 ¦ num2 |
按位或 | Int32 × Int32 → Int32 |
ES1 |
num1 ^ num2 |
按位异或 | Int32 × Int32 → Int32 |
ES1 |
二元按位运算符(表10)组合其操作数的位以生成结果。
> (0b1010 & 0b0011).toString(2).padStart(4, '0')'0010'
> (0b1010 | 0b0011).toString(2).padStart(4, '0')'1011'
> (0b1010 ^ 0b0011).toString(2).padStart(4, '0')'1001'
操作 | 名称 | 类型签名 | |
---|---|---|---|
num << count |
左移 | Int32 × Uint32 → Int32 |
ES1 |
num >> count |
有符号右移 | Int32 × Uint32 → Int32 |
ES1 |
num >>> count |
无符号右移 | Uint32 × Uint32 → Uint32 |
ES1 |
移位运算符(表11)将二进制位向左或向右移动。
> (0b10 << 1).toString(2)'100'
>>
保留最高位,>>>
不保留。
> b32(0b10000000000000000000000000000010 >> 1)'11000000000000000000000000000001'
> b32(0b10000000000000000000000000000010 >>> 1)'01000000000000000000000000000001'
b32()
:以二进制表示法显示无符号 32 位整数我们已经多次使用 b32()
。以下代码是其实现:
/**
* Return a string representing n as a 32-bit unsigned integer,
* in binary notation.
*/
function b32(n) {
// >>> ensures highest bit isn’t interpreted as a sign
return (n >>> 0).toString(2).padStart(32, '0');
}.equal(
assertb32(6),
'00000000000000000000000000000110');
n >>> 0
意味着我们将 n
向右移动零位。因此,原则上,>>>
运算符不做任何事情,但它仍然将 n
强制转换为无符号 32 位整数。
> 12 >>> 012
> -12 >>> 04294967284
> (2**32 + 1) >>> 01
JavaScript 有以下四个用于数字的全局函数:
isFinite()
isNaN()
parseFloat()
parseInt()
但是,最好使用 Number
的相应方法(Number.isFinite()
等),因为它们的问题更少。它们是在 ES6 中引入的,将在下面讨论。
Number
的静态属性.EPSILON: number
[ES6]
1 与下一个可表示浮点数之间的差。通常,机器ε提供了浮点运算中舍入误差的上限。
.MAX_SAFE_INTEGER: number
[ES6]
JavaScript 可以明确表示的最大整数 (253−1)。
.MAX_VALUE: number
[ES1]
最大的正有限 JavaScript 数字。
.MIN_SAFE_INTEGER: number
[ES6]
JavaScript 可以明确表示的最小整数 (−253+1)。
.MIN_VALUE: number
[ES1]
最小的正 JavaScript 数字。约为 5 × 10−324。
.NaN: number
[ES1]
与全局变量 NaN
相同。
.NEGATIVE_INFINITY: number
[ES1]
与 -Number.POSITIVE_INFINITY
相同。
.POSITIVE_INFINITY: number
[ES1]
与全局变量 Infinity
相同。
Number
的静态方法.isFinite(num: number): boolean
[ES6]
如果 num
是实际数字(既不是 Infinity
也不是 -Infinity
也不是 NaN
),则返回 true
。
> Number.isFinite(Infinity)false
> Number.isFinite(-Infinity)false
> Number.isFinite(NaN)false
> Number.isFinite(123)true
.isInteger(num: number): boolean
[ES6]
如果 num
是数字且没有小数部分,则返回 true
。
> Number.isInteger(-17)true
> Number.isInteger(33)true
> Number.isInteger(33.1)false
> Number.isInteger('33')false
> Number.isInteger(NaN)false
> Number.isInteger(Infinity)false
.isNaN(num: number): boolean
[ES6]
如果 num
是值 NaN
,则返回 true
。
> Number.isNaN(NaN)true
> Number.isNaN(123)false
> Number.isNaN('abc')false
.isSafeInteger(num: number): boolean
[ES6]
如果 num
是数字并且明确表示一个整数,则返回 true
。
.parseFloat(str: string): number
[ES6]
将其参数强制转换为字符串并将其解析为浮点数。对于将字符串转换为数字,Number()
(忽略开头和结尾的空格)通常是比 Number.parseFloat()
(忽略开头的空格和非法的结尾字符,并且可能隐藏问题)更好的选择。
> Number.parseFloat(' 123.4#')123.4
> Number(' 123.4#')NaN
.parseInt(str: string, radix=10): number
[ES6]
将其参数强制转换为字符串并将其解析为整数,忽略开头的空格和非法的结尾字符。
> Number.parseInt(' 123#')123
参数 radix
指定要解析的数字的基数。
> Number.parseInt('101', 2)5
> Number.parseInt('FF', 16)255
不要使用此方法将数字转换为整数:强制转换为字符串效率低下。 并且在第一个非数字之前停止并不是一个很好的算法来删除数字的小数部分。 这是一个出错的例子
> Number.parseInt(1e21, 10) // wrong1
最好使用 Math
的舍入函数之一将数字转换为整数
> Math.trunc(1e21) // correct1e+21
Number.prototype
的方法(Number.prototype
是存储数字方法的地方。)
.toExponential(fractionDigits?: number): string
[ES3]
返回一个字符串,该字符串通过指数表示法表示数字。 使用 fractionDigits
,我们可以指定应该显示与指数相乘的数字的位数(默认值是显示尽可能多的数字)。
示例:数字太小,无法通过 .toString()
获得正指数。
> 1234..toString()'1234'
> 1234..toExponential() // 3 fraction digits'1.234e+3'
> 1234..toExponential(5)'1.23400e+3'
> 1234..toExponential(1)'1.2e+3'
示例:分数不够小,无法通过 .toString()
获得负指数。
> 0.003.toString()'0.003'
> 0.003.toExponential()'3e-3'
.toFixed(fractionDigits=0): string
[ES3]
返回数字的无指数表示,四舍五入到 fractionDigits
位。
> 0.00000012.toString() // with exponent'1.2e-7'
> 0.00000012.toFixed(10) // no exponent'0.0000001200'
> 0.00000012.toFixed()'0'
如果数字是 1021 或更大,则即使是 .toFixed()
也会使用指数
> (10 ** 21).toFixed()'1e+21'
.toPrecision(precision?: number): string
[ES3]
与 .toString()
类似,但 precision
指定应显示多少位数字。 如果缺少 precision
,则使用 .toString()
。
> 1234..toPrecision(3) // requires exponential notation'1.23e+3'
> 1234..toPrecision(4)'1234'
> 1234..toPrecision(5)'1234.0'
> 1.234.toPrecision(3)'1.23'
.toString(radix=10): string
[ES1]
返回数字的字符串表示形式。
默认情况下,我们得到一个以 10 为基数的数字作为结果
> 123.456.toString()'123.456'
如果我们希望数字具有不同的基数,我们可以通过 radix
指定它
> 4..toString(2) // binary (base 2)'100'
> 4.5.toString(2)'100.1'
> 255..toString(16) // hexadecimal (base 16)'ff'
> 255.66796875.toString(16)'ff.ab'
> 1234567890..toString(36)'kf12oi'
Number.parseInt()
提供了逆运算:它将包含具有给定基数的整数(无小数!)数字的字符串转换为数字。
> Number.parseInt('kf12oi', 36)1234567890
测验:高级
请参阅测验应用程序。