面向急切程序员的 JavaScript(ES2022 版)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

16 数字



JavaScript 有两种数值类型

本章介绍数字。BigInt 在本书后面介绍。

16.1 数字用于表示浮点数和整数

在 JavaScript 中,number 类型用于表示整数和浮点数

98
123.45

但是,所有数字都是双精度的,即根据IEEE 浮点算术标准 (IEEE 754) 实现的 64 位浮点数。

整数只是没有小数部分的浮点数

> 98 === 98.0
true

请注意,在底层,大多数 JavaScript 引擎通常能够使用真正的整数,并具有所有相关的性能和存储大小优势。

16.2 数字字面量

让我们来看看数字的字面量。

16.2.1 整数字面量

几种整数字面量允许我们用不同的进制表示整数

// Binary (base 2)
assert.equal(0b11, 3); // ES6

// Octal (base 8)
assert.equal(0o10, 8); // ES6

// Decimal (base 10)
assert.equal(35, 35);

// Hexadecimal (base 16)
assert.equal(0xE7, 231);

16.2.2 浮点数字面量

浮点数只能用十进制表示。

分数

> 35.0
35

指数:eN 表示 ×10N

> 3e2
300
> 3e-2
0.03
> 0.3e2
30

16.2.3 语法陷阱:整数字面量的属性

访问整数字面量的属性会导致一个陷阱:如果整数字面量后面紧跟着一个点,那么该点会被解释为小数点

7.toString(); // syntax error

有四种方法可以解决这个陷阱

7.0.toString()
(7).toString()
7..toString()
7 .toString()  // space before dot

16.2.4 数字字面量中的下划线 (_) 分隔符 [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;
16.2.4.1 我们可以在哪里放置分隔符?

分隔符的位置受到两种方式的限制

这些限制背后的动机是保持解析简单并避免奇怪的边缘情况。

16.2.4.2 解析带有分隔符的数字

以下用于解析数字的函数不支持分隔符

例如

> Number('123_456')
NaN
> Number.parseInt('123_456')
123

其理由是数字分隔符是用于代码的。其他类型的输入应该以不同的方式处理。

16.3 算术运算符

16.3.1 二元算术运算符

表 5 列出了 JavaScript 的二元算术运算符。

表 5:二元算术运算符。
运算符 名称 示例
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
16.3.1.1 % 是取余运算符

% 是取余运算符,而不是取模运算符。其结果的符号与第一个操作数相同

> 5 % 3
2
> -5 % 3
-2

有关取余和取模之间区别的更多信息,请参阅 2ality 上的博客文章 “取余运算符与取模运算符(使用 JavaScript 代码)”

16.3.2 一元加号 (+) 和一元减号 (-)

表 6 总结了两个运算符:一元加号 (+) 和一元减号 (-)。

表 6:一元加号 (+) 和一元减号 (-) 运算符。
运算符 名称 示例
+n 一元加号 ES1 +(-7) -7
-n 一元减号 ES1 -(-7) 7

这两个运算符都会将其操作数强制转换为数字

> +'5'
5
> +'-12'
-12
> -'9'
-9

因此,一元加号允许我们将任意值转换为数字。

16.3.3 递增 (++) 和递减 (--)

递增运算符 ++ 存在前缀版本和后缀版本。在这两个版本中,它都会破坏性地将其操作数加 1。因此,其操作数必须是可以更改的存储位置。

递减运算符 -- 的工作原理相同,但会将其操作数减 1。接下来的两个示例解释了前缀版本和后缀版本之间的区别。

表 7 总结了递增和递减运算符。

表 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;
assert.equal(++foo, 4);
assert.equal(foo, 4);

let bar = 3;
assert.equal(--bar, 2);
assert.equal(bar, 2);

后缀 ++ 和后缀 -- 会先返回其操作数,然后更改其值。

let foo = 3;
assert.equal(foo++, 3);
assert.equal(foo, 4);

let bar = 3;
assert.equal(bar--, 3);
assert.equal(bar, 2);
16.3.3.1 操作数:不仅仅是变量

我们也可以将这些运算符应用于属性值

const obj = { a: 1 };
++obj.a;
assert.equal(obj.a, 2);

以及数组元素

const arr = [ 4 ];
arr[0]++;
assert.deepEqual(arr, [5]);

  练习:数字运算符

exercises/numbers-math/is_odd_test.mjs

16.4 转换为数字

以下是将值转换为数字的三种方法

建议:使用描述性的 Number()。表 8 总结了它的工作原理。

表 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()

示例

assert.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);

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

> Number({ valueOf() { return 123 } })
123

  练习:转换为数字

exercises/numbers-math/parse_number_test.mjs

16.5 错误值

发生错误时会返回两个数字值

16.5.1 错误值: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 - 3
NaN
> 7 ** NaN
NaN
16.5.1.1 检查 NaN

NaN 是唯一一个不严格等于自身的 JavaScript 值

const n = NaN;
assert.equal(n === n, false);

以下几种方法可以检查值 x 是否为 NaN

const x = NaN;

assert.equal(Number.isNaN(x), true); // preferred
assert.equal(Object.is(x, NaN), true);
assert.equal(x !== x, true);

在最后一行中,我们使用比较怪癖来检测 NaN

16.5.1.2 在数组中查找 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

16.5.2 错误值:Infinity

什么时候会返回错误值 Infinity

如果数字太大,则返回 Infinity

> Math.pow(2, 1023)
8.98846567431158e+307
> Math.pow(2, 1024)
Infinity

如果除以零,则返回 Infinity

> 5 / 0
Infinity
> -5 / 0
-Infinity
16.5.2.1 Infinity 作为默认值

Infinity 比所有其他数字都大(除了 NaN),这使其成为一个很好的默认值

function findMinimum(numbers) {
  let min = Infinity;
  for (const n of numbers) {
    if (n < min) min = n;
  }
  return min;
}

assert.equal(findMinimum([5, -1, 2]), -1);
assert.equal(findMinimum([]), Infinity);
16.5.2.2 检查 Infinity

以下两种常见方法可以检查值 x 是否为 Infinity

const x = Infinity;

assert.equal(x === Infinity, true);
assert.equal(Number.isFinite(x), false);

  练习:比较数字

exercises/numbers-math/find_max_test.mjs

16.6 数字的精度:小心处理小数

在内部,JavaScript 浮点数使用二进制表示(根据 IEEE 754 标准)。这意味着十进制小数并不总是能够精确表示

> 0.1 + 0.2
0.30000000000000004
> 1.3 * 3
3.9000000000000004
> 1.4 * 100000000000000
139999999999999.98

因此,在 JavaScript 中执行算术运算时,我们需要考虑舍入误差。

继续阅读以了解此现象的解释。

  测验:基础

请参阅测验应用程序

16.7 (进阶)

本章的其余部分均为进阶内容。

16.8 背景:浮点数精度

在 JavaScript 中,使用数字进行计算并不总是产生正确的结果,例如

> 0.1 + 0.2
0.30000000000000004

要理解原因,我们需要了解 JavaScript 如何在内部表示浮点数。它使用三个整数来表示,总共占用 64 位存储空间(双精度)

组成部分 大小 整数范围
符号 1 位 [0, 1]
分数 52 位 [0, 252−1]
指数 11 位 [−1023, 1024]

由这些整数表示的浮点数的计算方法如下:

(–1)符号位 × 0b1.小数部分 × 2指数

这种表示方法无法编码零,因为它的第二部分(涉及小数部分)始终以 1 开头。因此,零是通过特殊指数 -1023 和小数部分 0 来编码的。

16.8.1 浮点数的简化表示

为了使进一步的讨论更容易,我们简化了之前的表示方法。

新的表示方法如下:

尾数 × 10指数

让我们用这种表示方法来表示几个浮点数。

具有负指数的表示形式也可以写成分母中具有正指数的分数。

> 15 * (10 ** -1) === 15 / (10 ** 1)
true
> 25 * (10 ** -2) === 25 / (10 ** 2)
true

这些分数有助于理解为什么有些数字我们的编码无法表示。

为了结束我们的探索,我们回到以 2 为底。

现在我们可以看到为什么 0.1 + 0.2 不会产生正确的结果:在内部,这两个操作数都不能精确表示。

使用十进制小数进行精确计算的唯一方法是在内部切换到以 10 为底。对于许多编程语言来说,以 2 为底是默认的,以 10 为底是一个选项。例如,Java 有 BigDecimal 类,Python 有 decimal 模块。JavaScript 也计划添加类似的功能:ECMAScript 提案“Decimal”

16.9 JavaScript 中的整数

整数是没有小数部分的普通(浮点)数。

> 1 === 1.0
true
> Number.isInteger(1.0)
true

在本节中,我们将介绍一些用于处理这些伪整数的工具。JavaScript 还支持大整数,它们是真正的整数。

16.9.1 转换为整数

将数字转换为整数的推荐方法是使用 Math 对象的舍入方法之一。

有关舍入的更多信息,请参阅§17.3 “舍入”

16.9.2 JavaScript 中的整数范围

以下是 JavaScript 中重要的整数范围:

16.9.3 安全整数

这是 JavaScript 中安全的整数范围(53 位加符号位)。

[–(253)+1, 253–1]

如果一个整数由一个 JavaScript 数字精确表示,则它是安全的。鉴于 JavaScript 数字被编码为一个分数乘以 2 的指数幂,因此也可以表示更大的整数,但它们之间存在间隙。

例如(18014398509481984 是 254):

> 18014398509481984
18014398509481984
> 18014398509481985
18014398509481984
> 18014398509481986
18014398509481984
> 18014398509481987
18014398509481988

Number 的以下属性有助于确定一个整数是否安全:

assert.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);

  练习:检测安全整数

exercises/numbers-math/is_safe_integer_test.mjs

16.9.3.1 安全计算

让我们看看涉及不安全整数的计算。

以下结果是不正确且不安全的,即使它的两个操作数都是安全的:

> 9007199254740990 + 3
9007199254740992

以下结果是安全的,但不正确。第一个操作数是不安全的;第二个操作数是安全的:

> 9007199254740995 - 10
9007199254740986

因此,表达式 a op b 的结果正确,当且仅当:

isSafeInteger(a) && isSafeInteger(b) && isSafeInteger(a op b)

也就是说,两个操作数和结果都必须是安全的。

16.10 按位运算符

16.10.1 在内部,按位运算符使用 32 位整数

在内部,JavaScript 的按位运算符使用 32 位整数。它们按以下步骤生成结果:

16.10.1.1 操作数和结果的类型

对于每个按位运算符,本书都提到了其操作数和结果的类型。每种类型始终是以下两种之一:

类型 描述 大小 范围
Int32 有符号 32 位整数 32 位(包括符号位) [−231, 231)
Uint32 无符号 32 位整数 32 位 [0, 232)

考虑到前面提到的步骤,我建议假装按位运算符在内部使用无符号 32 位整数(“计算”步骤),而 Int32 和 Uint32 只影响 JavaScript 数字如何转换为整数以及从整数转换(“输入”和“输出”步骤)。

16.10.1.2 以无符号 32 位整数形式显示 JavaScript 数字

在探索按位运算符时,偶尔以二进制表示法显示无符号 32 位整数形式的 JavaScript 数字会有所帮助。这就是 b32() 的作用(其实现将在后面展示)。

assert.equal(
  b32(-1),
  '11111111111111111111111111111111');
assert.equal(
  b32(1),
  '00000000000000000000000000000001');
assert.equal(
  b32(2 ** 31),
  '10000000000000000000000000000000');

16.10.2 按位非

表 9:按位非运算符。
操作 名称 类型签名
~num 按位非,反码 Int32 Int32 ES1

按位非运算符(表9)反转其操作数的每个二进制位。

> b32(~0b100)
'11111111111111111111111111111011'

这种所谓的反码对于某些算术运算类似于负数。例如,将一个整数与其反码相加始终为 -1

> 4 + ~4
-1
> -11 + ~-11
-1

16.10.3 二元按位运算符

表 10:二元按位运算符。
操作 名称 类型签名
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'

16.10.4 按位移位运算符

表 11:按位移位运算符。
操作 名称 类型签名
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'

16.10.5 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');
}
assert.equal(
  b32(6),
  '00000000000000000000000000000110');

n >>> 0 意味着我们将 n 向右移动零位。因此,原则上,>>> 运算符不做任何事情,但它仍然将 n 强制转换为无符号 32 位整数。

> 12 >>> 0
12
> -12 >>> 0
4294967284
> (2**32 + 1) >>> 0
1

16.11 快速参考:数字

16.11.1 数字的全局函数

JavaScript 有以下四个用于数字的全局函数:

但是,最好使用 Number 的相应方法(Number.isFinite() 等),因为它们的问题更少。它们是在 ES6 中引入的,将在下面讨论。

16.11.2 Number 的静态属性

16.11.3 Number 的静态方法

16.11.4 Number.prototype 的方法

Number.prototype 是存储数字方法的地方。)

16.11.5 来源

  测验:高级

请参阅测验应用程序