在现代 JavaScript 中,有两种类型的数字
-
JavaScript 中的常规数字存储在 64 位格式 IEEE-754 中,也称为“双精度浮点数”。这些是我们大多数时间使用的数字,我们将在本章中讨论它们。
-
BigInt 数字表示任意长度的整数。有时需要它们,因为常规整数不能安全地超过
(253-1)
或小于-(253-1)
,正如我们在本章 数据类型 中提到的那样。由于 bigint 用于一些特殊领域,因此我们专门为它们编写了一个章节 BigInt。
因此,这里我们将讨论常规数字。让我们扩展对此类数字的了解。
更多编写数字的方法
设想我们需要编写 10 亿。显而易见的方法是
let billion = 1000000000;
我们还可以使用下划线 _
作为分隔符
let billion = 1_000_000_000;
此处,下划线 _
扮演“语法糖”的角色,它使数字更具可读性。JavaScript 引擎简单地忽略数字之间的 _
,因此它与上述十亿完全相同。
然而,在现实生活中,我们尝试避免编写长序列的零。我们对此感到太懒惰。我们将尝试为十亿编写类似 "1bn"
的内容,或为 73 亿编写 "7.3bn"
。对于大多数大数字而言,情况也是如此。
在 JavaScript 中,我们可以通过在数字后附加字母 "e"
并指定零计数来缩短数字
let billion = 1e9; // 1 billion, literally: 1 and 9 zeroes
alert( 7.3e9 ); // 7.3 billions (same as 7300000000 or 7_300_000_000)
换句话说,e
将数字乘以给定零计数的 1
。
1e3 === 1 * 1000; // e3 means *1000
1.23e6 === 1.23 * 1000000; // e6 means *1000000
现在,让我们编写一些非常小的内容。比如,1 微秒(百万分之一秒)
let mсs = 0.000001;
就像之前一样,使用 "e"
可以提供帮助。如果我们希望避免显式编写零,我们可以编写与
let mcs = 1e-6; // five zeroes to the left from 1
相同的内容。如果我们计算 0.000001
中的零,则有 6 个零。因此,它自然地是 1e-6
。
换句话说,"e"
后面的负数表示除以给定零数的 1
// -3 divides by 1 with 3 zeroes
1e-3 === 1 / 1000; // 0.001
// -6 divides by 1 with 6 zeroes
1.23e-6 === 1.23 / 1000000; // 0.00000123
// an example with a bigger number
1234e-2 === 1234 / 100; // 12.34, decimal point moves 2 times
十六进制、二进制和八进制数字
十六进制数字在 JavaScript 中被广泛用于表示颜色、编码字符以及许多其他内容。因此,自然存在一种更简短的编写方式:0x
,然后是数字。
例如
alert( 0xff ); // 255
alert( 0xFF ); // 255 (the same, case doesn't matter)
二进制和八进制数字系统很少使用,但也可以使用 0b
和 0o
前缀进行支持
let a = 0b11111111; // binary form of 255
let b = 0o377; // octal form of 255
alert( a == b ); // true, the same number 255 at both sides
只有 3 个数字系统具有此类支持。对于其他数字系统,我们应该使用函数 parseInt
(我们将在本章后面看到)。
toString(base)
方法 num.toString(base)
返回 num
在具有给定 base
的数字系统中的字符串表示形式。
例如
let num = 255;
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
base
可以从 2
到 36
变化。默认情况下,它是 10
。
此功能的常见用例是
-
base=16 用于十六进制颜色、字符编码等,数字可以是
0..9
或A..F
。 -
base=2 主要用于调试按位操作,数字可以是
0
或1
。 -
base=36 是最大值,数字可以是
0..9
或A..Z
。整个拉丁字母表用于表示一个数字。36
的一个有趣但有用的情况是当我们需要将一个长的数字标识符变成一个较短的标识符时,例如,为了生成一个短网址。可以简单地用 base 为36
的数字系统表示它alert( 123456..toString(36) ); // 2n9c
请注意,123456..toString(36)
中的两个点不是错字。如果我们想直接对一个数字调用一个方法,如上面的示例中的 toString
,那么我们需要在其后放置两个点 ..
。
如果我们放置一个点:123456.toString(36)
,那么就会出现一个错误,因为 JavaScript 语法暗示第一个点之后的小数部分。如果我们再放置一个点,那么 JavaScript 就知道小数部分为空,现在转到方法。
还可以写成 (123456).toString(36)
。
舍入
在使用数字时,最常用的操作之一是舍入。
有几个用于舍入的内置函数
Math.floor
- 向下舍入:
3.1
变成3
,-1.1
变成-2
。 Math.ceil
- 向上舍入:
3.1
变成4
,-1.1
变成-1
。 Math.round
- 舍入到最接近的整数:
3.1
变成3
,3.6
变成4
,中间情况:3.5
也向上舍入到4
。 Math.trunc
(不受 Internet Explorer 支持)- 删除小数点后的所有内容,不进行舍入:
3.1
变成3
,-1.1
变成-1
。
下面是总结它们之间差异的表格
Math.floor |
Math.ceil |
Math.round |
Math.trunc |
|
---|---|---|---|---|
3.1 |
3 |
4 |
3 |
3 |
3.6 |
3 |
4 |
4 |
3 |
-1.1 |
-2 |
-1 |
-1 |
-1 |
-1.6 |
-2 |
-1 |
-2 |
-1 |
这些函数涵盖了处理数字小数部分的所有可能方式。但是,如果我们想将数字舍入到小数点后第 n
位怎么办?
例如,我们有 1.2345
,并希望将其舍入到 2 位,只得到 1.23
。
有两种方法可以做到这一点
-
乘除法。
例如,要将数字舍入到小数点后第 2 位,我们可以将数字乘以
100
,调用舍入函数,然后将其除回去。let num = 1.23456; alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
-
方法 toFixed(n) 将数字舍入到小数点后
n
位,并返回结果的字符串表示形式。let num = 12.34; alert( num.toFixed(1) ); // "12.3"
这会向上或向下舍入到最接近的值,类似于
Math.round
let num = 12.36; alert( num.toFixed(1) ); // "12.4"
请注意,
toFixed
的结果是一个字符串。如果小数部分比要求的短,则会在末尾追加零let num = 12.34; alert( num.toFixed(5) ); // "12.34000", added zeroes to make exactly 5 digits
我们可以使用一元加号或
Number()
调用将其转换为数字,例如,写成+num.toFixed(5)
。
不精确的计算
在内部,一个数字以 64 位格式 IEEE-754 表示,因此有正好 64 位来存储一个数字:其中 52 位用于存储数字,11 位用于存储小数点的点,1 位用于符号。
如果一个数字非常大,它可能会溢出 64 位存储空间并变成一个特殊数字值 Infinity
alert( 1e500 ); // Infinity
可能不太明显,但经常发生的是精度损失。
考虑这个(错误的!)相等性测试
alert( 0.1 + 0.2 == 0.3 ); // false
没错,如果我们检查 0.1
和 0.2
的和是否为 0.3
,我们会得到 false
。
奇怪!如果不是 0.3
,那是什么呢?
alert( 0.1 + 0.2 ); // 0.30000000000000004
哎呀!想象一下,你正在做一个电子购物网站,访问者将 $0.10
和 $0.20
的商品放入他们的购物车。订单总额将为 $0.30000000000000004
。这会让任何人感到惊讶。
但为什么会发生这种情况呢?
一个数字以其二进制形式存储在内存中,即一系列位——1 和 0。但是像 0.1
、0.2
这样的分数在十进制数字系统中看起来很简单,实际上在它们的二进制形式中是无穷分数。
alert(0.1.toString(2)); // 0.0001100110011001100110011001100110011001100110011001101
alert(0.2.toString(2)); // 0.001100110011001100110011001100110011001100110011001101
alert((0.1 + 0.2).toString(2)); // 0.0100110011001100110011001100110011001100110011001101
什么是 0.1
?它是十进制 1/10
,十分之一。在十进制数字系统中,这样的数字很容易表示。将它与三分之一进行比较:1/3
。它变成了一个无穷分数 0.33333(3)
。
因此,在十进制系统中,除以 10
的幂可以保证正常工作,但除以 3
则不行。出于同样的原因,在二进制数字系统中,除以 2
的幂可以保证正常工作,但 1/10
变成一个无穷二进制分数。
就像无法将三分之一存储为十进制分数一样,使用二进制系统根本无法存储 正好 0.1 或 正好 0.2。
数字格式 IEEE-754 通过舍入到最近的可能数字来解决这个问题。这些舍入规则通常不允许我们看到“微小的精度损失”,但它确实存在。
我们可以看到它的实际情况
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
当我们对两个数字求和时,它们的“精度损失”会累加。
这就是为什么 0.1 + 0.2
不等于 0.3
。
许多其他编程语言中都存在相同的问题。
PHP、Java、C、Perl 和 Ruby 给出的结果完全相同,因为它们基于相同的数字格式。
我们能解决这个问题吗?当然,最可靠的方法是使用 toFixed(n) 方法舍入结果
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // "0.30"
请注意,toFixed
始终返回一个字符串。它确保小数点后有 2 位数字。如果我们有电子商务并需要显示 $0.30
,这实际上很方便。对于其他情况,我们可以使用一元加号将其强制转换为数字
let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3
我们还可以暂时将数字乘以 100(或更大的数字)以将它们转换为整数,进行数学运算,然后除回去。然后,由于我们使用整数进行数学运算,因此误差会略微减小,但我们仍然会在除法中得到它
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
因此,乘除法方法减少了误差,但并未完全消除它。
有时我们可以尝试完全避开分数。比如,如果我们经营一家商店,那么我们可以将价格存储为美分,而不是美元。但是,如果我们应用 30% 的折扣怎么办?在实践中,完全避开分数很少可能。在需要时,只需将其四舍五入以剪掉“尾巴”。
尝试运行此操作
// Hello! I'm a self-increasing number!
alert( 9999999999999999 ); // shows 10000000000000000
这会遇到相同的问题:精度丢失。该数字有 64 位,其中 52 位可用于存储数字,但这还不够。因此,最低有效位消失了。
JavaScript 不会在这样的事件中触发错误。它会尽力将数字放入所需的格式,但不幸的是,此格式不够大。
数字内部表示的另一个有趣后果是存在两个零:0
和 -0
。
这是因为符号由一个比特表示,因此可以为任何数字(包括零)设置或不设置它。
在大多数情况下,这种区别并不明显,因为运算符适合将它们视为相同。
测试:isFinite 和 isNaN
还记得这两个特殊的数字值吗?
Infinity
(和-Infinity
)是一个特殊数字值,大于(小于)任何东西。NaN
表示错误。
它们属于 number
类型,但不是“普通”数字,因此有专门的函数来检查它们
-
isNaN(value)
将其参数转换为数字,然后测试它是否为NaN
alert( isNaN(NaN) ); // true alert( isNaN("str") ); // true
但我们需要这个函数吗?我们不能直接使用比较
=== NaN
吗?不幸的是,不行。NaN
值的独特之处在于它不等于任何东西,包括它自己alert( NaN === NaN ); // false
-
isFinite(value)
将其参数转换为数字,如果它是一个常规数字,而不是NaN/Infinity/-Infinity
,则返回true
alert( isFinite("15") ); // true alert( isFinite("str") ); // false, because a special value: NaN alert( isFinite(Infinity) ); // false, because a special value: Infinity
有时 isFinite
用于验证字符串值是否为常规数字
let num = +prompt("Enter a number", '');
// will be true unless you enter Infinity, -Infinity or not a number
alert( isFinite(num) );
请注意,在所有数字函数(包括 isFinite
)中,空字符串或仅包含空格的字符串都将被视为 0
。
Number.isNaN
和 Number.isFinite
Number.isNaN 和 Number.isFinite 方法是 isNaN
和 isFinite
函数的更“严格”版本。它们不会自动将参数转换为数字,而是检查它是否属于 number
类型。
-
如果参数属于
number
类型并且它是NaN
,则Number.isNaN(value)
返回true
。在任何其他情况下,它返回false
。alert( Number.isNaN(NaN) ); // true alert( Number.isNaN("str" / 2) ); // true // Note the difference: alert( Number.isNaN("str") ); // false, because "str" belongs to the string type, not the number type alert( isNaN("str") ); // true, because isNaN converts string "str" into a number and gets NaN as a result of this conversion
-
如果参数属于
number
类型并且它不是NaN/Infinity/-Infinity
,则Number.isFinite(value)
返回true
。在任何其他情况下,它返回false
。alert( Number.isFinite(123) ); // true alert( Number.isFinite(Infinity) ); // false alert( Number.isFinite(2 / 0) ); // false // Note the difference: alert( Number.isFinite("123") ); // false, because "123" belongs to the string type, not the number type alert( isFinite("123") ); // true, because isFinite converts string "123" into a number 123
在某种程度上,Number.isNaN
和 Number.isFinite
比 isNaN
和 isFinite
函数更简单、更直接。然而,在实践中,isNaN
和 isFinite
使用得最多,因为它们更短。
Object.is
的比较有一个特殊的内置方法 Object.is
,它像 ===
一样比较值,但对于两种边缘情况更可靠
- 它适用于
NaN
:Object.is(NaN, NaN) === true
,这是一件好事。 - 值
0
和-0
是不同的:Object.is(0, -0) === false
,从技术上讲这是正确的,因为内部数字有一个符号位,即使所有其他位都是零,它也可能不同。
在所有其他情况下,Object.is(a, b)
与 a === b
相同。
我们在这里提到 Object.is
,因为它经常在 JavaScript 规范中使用。当内部算法需要比较两个值是否完全相同时,它使用 Object.is
(内部称为 SameValue)。
parseInt 和 parseFloat
使用加号 +
或 Number()
进行数字转换是严格的。如果一个值不是一个数字,它将失败
alert( +"100px" ); // NaN
唯一的例外是字符串开头或结尾的空格,因为它们会被忽略。
但在现实生活中,我们经常有单位值,比如 CSS 中的 "100px"
或 "12pt"
。此外,在许多国家,货币符号位于金额之后,因此我们有 "19€"
,并且希望从中提取一个数字值。
这就是 parseInt
和 parseFloat
的作用。
它们从一个字符串中“读取”一个数字,直到它们不能读取为止。如果发生错误,则返回收集到的数字。函数 parseInt
返回一个整数,而 parseFloat
将返回一个浮点数
alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5
alert( parseInt('12.3') ); // 12, only the integer part is returned
alert( parseFloat('12.3.4') ); // 12.3, the second point stops the reading
在 parseInt/parseFloat
将返回 NaN
的情况下。当无法读取数字时会发生这种情况
alert( parseInt('a123') ); // NaN, the first symbol stops the process
parseInt(str, radix)
的第二个参数parseInt()
函数有一个可选的第二个参数。它指定数字系统的基数,因此 parseInt
还可以解析十六进制数字、二进制数字等字符串
alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255, without 0x also works
alert( parseInt('2n9c', 36) ); // 123456
其他数学函数
JavaScript 有一个内置的 Math 对象,其中包含一个小型数学函数和常量的库。
几个示例
Math.random()
-
返回 0 到 1 之间的随机数(不包括 1)。
alert( Math.random() ); // 0.1234567894322 alert( Math.random() ); // 0.5435252343232 alert( Math.random() ); // ... (any random numbers)
Math.max(a, b, c...)
和Math.min(a, b, c...)
-
从任意数量的参数中返回最大值和最小值。
alert( Math.max(3, 5, -10, 0, 1) ); // 5 alert( Math.min(1, 2) ); // 1
Math.pow(n, power)
-
返回
n
乘以给定幂的结果。alert( Math.pow(2, 10) ); // 2 in power 10 = 1024
Math
对象中还有更多函数和常量,包括三角函数,你可以在 Math 对象的文档 中找到它们。
总结
要编写带有许多零的数字
- 在数字后附加
"e"
和零的计数。比如:123e6
等于123
后面有 6 个零123000000
。 "e"
后面的负数会导致数字除以 1 并带有给定的零。例如,123e-6
表示0.000123
(123 百万分之一)。
对于不同的数字系统
- 可以直接用十六进制 (
0x
)、八进制 (0o
) 和二进制 (0b
) 系统编写数字。 parseInt(str, base)
将字符串str
解析为给定base
的数字系统中的整数,2 ≤ base ≤ 36
。num.toString(base)
将数字转换为给定base
的数字系统中的字符串。
对于常规数字测试
isNaN(value)
将其参数转换为数字,然后测试它是否为NaN
Number.isNaN(value)
检查其参数是否属于number
类型,如果是,则测试其是否为NaN
isFinite(value)
将其参数转换为数字,然后测试其是否不为NaN/Infinity/-Infinity
Number.isFinite(value)
检查其参数是否属于number
类型,如果是,则测试其是否不为NaN/Infinity/-Infinity
用于将 12pt
和 100px
等值转换为数字
- 对“软”转换使用
parseInt/parseFloat
,它从字符串中读取数字,然后返回它们在错误之前可以读取的值。
对于分数
- 使用
Math.floor
、Math.ceil
、Math.trunc
、Math.round
或num.toFixed(precision)
进行舍入。 - 务必记住在使用分数时会损失精度。
更多数学函数
- 在需要时,请参阅 Math 对象。该库非常小,但可以满足基本需求。
评论
<code>
标记,对于多行 - 将其包装在<pre>
标记中,对于超过 10 行 - 使用沙盒 (plnkr,jsbin,codepen…)