原文链接:http://javascript.info/number,translate with ❤️ by zhangbao.

JavaScript 中的数字都是按照 IEEE-754 标准,底层用 64 位格式在计算机中表示的,也就是我们所说的“双精度”。

我们来回顾一下我们目前所知道的关于数字的知识。

书写数字的多种方式

假如我们要写 1 亿这个数字,最直接的方式是这样的:

  1. let billion = 1000000000;

在实际生活里,我们通常避免使用这种需要写一长串 0 的方式,这很容易导致录入错误。为了偷懒,我们通常写作“1亿”或者“7.3”亿这样子,对待大数几乎都是这样的。

在 JavaScript 中,通过在数字后面添加“e”来表示 10 的次幂,也就是 0 的数量,来简写大数:

  1. let billion = 1e9; // 1 亿, 字面上就是:1 和 9 个 0
  2. alert( 7.3e9 ); // 7.3 亿(7,300,000,000)

换句话说,“e”代表乘以 10 的几次幂:

  1. 1e3 = 1 * 1000
  2. 1.23e6 = 1.23 * 1000000

现在我们写一个非常小的数。比如说,1 微秒(一百万分之一秒):

  1. let us = 0.000001;

类似之前,可以用“e”来帮忙:

  1. let ms = 1e-6; // 1 左边有 6 个 0

我们如果数 0.000001 里的零,发现有 6 个,也就写成 1e-6 了。

换句话说,“e”后面的负数(比如 -x),表示 1/10 的意思。

  1. // -3 表示 1 除以1和3个0
  2. 1e-3 = 1 / 1000 (=0.001)
  3. // -6 表示 1 除以1和6个0
  4. 1.23e-6 = 1.23 / 1000000 (=0.00000123)

十六进制、二进制和八进制数字

在 JavaScript 里我们通常使用十六进制来表示颜色,编码字符和做其他事情。所以很自然地,有一种更短的方法来写它们:以 0x 最为前缀跟上数字。

例如:

  1. alert( 0xff ); // 255
  2. alert( 0xFF ); // 255 (一样的,不区分大小写)

二进制和八进制数字很少使用,但是也可以通过 0b 和 0o 前缀来支持:

  1. let a = 0b11111111; // 二进制的 255
  2. let b = 0o377; // 八进制的 255
  3. alert( a == b ); // 两者相等,都表示 255

数字字面量仅支持这 3 种类型的数字系统的声明。对于其他的数字系统,我们刻意使用 parseInt 函数(会在本章稍后的地方涉及到)。

toString(base)

num.toString(base) 返回指定数字 num 的以 base 基数(即“进制”)的表现形式:

例如:

  1. let num = 255;
  2. alert( num.toString(16) ); // ff
  3. alert( num.toString(2) ); // 11111111

基数可以使用从 2 到 36 的之间的整数。默认是 10。

常用的基数字面量有:

  • 16:用来表示颜色的 16 进制、或者字符的编码。取值从数字从 0~9、字母的 A~F。

  • 2: 主要用于调试位操作,可取数字是 0 和 1。

  • 36:是最大的可取值,得出的结果是由 0~9、A~Z 这个集合里字符组合而成的,整个拉丁字母序列表用来表示一个数字。对于 36 来说,一个有趣但有用的例子是当我们需要将一个长数字标识符转换成更短字符表示的时候。例如,用来创建一个短地址。可以在数字系统中简单地表示成基数是 36 位的表示:

  1. 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,-1.1 变成 -1。

Math.trunc

截取数字的整数部分,不发生舍入: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。

有两种方式可以做到:

  1. 乘之后再除

例如,在小数点后面的第2位,我们可以把数字乘以 100,调用舍入函数,然后再除以它。

  1. let num = 1.23456;
  2. alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
  1. 方法 toFixed(n) 在点之后将数字发送到 n 个数字,并返回结果的字符串表示。
  1. let num = 12.34;
  2. alert( num.toFixed(1) ); // "12.3"

这是向上或向下到最近的值,类似于 Math.round 方法:

  1. let num = 12.36;
  2. alert( num.toFixed(1) ); // "12.4"

请注意,toFixed 的结果是一个字符串。如果小数部分比所需的短,则将 0 加到末尾:

  1. let num = 12.34;
  2. alert( num.toFixed(5) ); // "12.34000", added zeroes to make exactly 5 digits

我们可以使用二进制加号或调用 Number() 将结果转换为数字:+num.toFixed(5)。

不精确的计算

在内部,一个数字是按照 IEEE-754 标准以 64 位格式存储在计算机中的:52 位是用来存储数字,其中 11 位存储小数(他们是零为整型数字)和1位符号。

如果一个数字太大,它就会溢出64位的存储空间,可能会给出一个无穷大:

  1. alert( 1e500 ); // Infinity

可能不那么明显,但经常发生的是,精确度的丧失。

认为这(falsy!)测试:

  1. alert( 0.1 + 0.2 == 0.3 ); // false

这是对的,如果我们检查 0.1 和 0.2 的和是 0.3,就会得到假。

奇怪!如果不是 0.3,它是什么?

  1. alert( 0.1 + 0.2 ); // 0.30000000000000004

哎哟!这里的结果比不正确的比较多。假设你正在做一个电子购物网站,访问者会在他的图表中放入0。10美元和0。20美元的商品。订单总数将为0.3000000000004美元。会惊讶。

一个数字以二进制的形式存储在内存中,一个1和0的序列。但是像0。1,0。2这样的分数在小数的数字系统中看起来很简单,在它们的二进制形式中是无穷无尽的分数。

换句话说,0。1是多少?它是1除以10 1/10,1/10。在十进制数字系统中,这样的数字很容易被表示。把它和三分之一:1/3。它变成了无穷无尽的分数0。33333(3)。

所以,10的除法保证在小数系统中很好,但是除以3不是。出于同样的原因,在二进制数字系统中,2的幂次被保证可以工作,但是1/10变成了一个无穷小的二进制分数。

没有办法用二进制系统来存储0。1或0。2,就像没有办法把三分之一的十进制数存储起来一样。

数字格式IEEE-754通过四舍五入到最接近的数字来解决这个问题。这些舍入规则通常不允许我们看到“微小的精度损失”,所以这个数字显示为0。3。但要注意,损失仍然存在。

我们可以在行动中看到这一点:

  1. alert(0.1.toFixed(20));/ / 0.10000000000000000555

当我们把两个数字加起来时,它们的“精度损失”加起来。

这就是为什么 0.1+0.2不正好是 0.3。

tip: 不仅是 JavaScript

在许多其他编程语言中也存在同样的问题。

PHP,Java,C,Perl,Ruby给出了完全相同的结果,因为它们是基于相同的数字格式。

我们能解决这个问题吗?当然,有很多方法:

  1. 我们可以使用 toFixed(n) 方法的帮助下解决这个问题:
  1. let sum = 0.1 + 0.2;
  2. alert( sum.toFixed(2) ); // 0.30

请注意,to固定式总是返回一个字符串。它确保小数点后有两位数字。这实际上很方便,如果我们有电子购物,需要显示0。30美元。对于其他情况,我们可以使用unary plus强制它成为一个数字:

  1. let sum = 0.1 + 0.2;
  2. alert( +sum.toFixed(2) ); // 0.3
  1. 我们可以暂时把数字转换成整数,然后再把它还原。是这样的:
  1. alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3

这是可行的,因为当我们做0。10=1和0。2 10=2时,两个数都变成整数,而且没有精确的损失。

  1. 如果我们在处理一家商店,那么最激进的解决方案就是把所有的价格都用美分来储存,而不用任何分数。但是如果我们用30%的折扣呢?在实践中,完全规避分数是不可能的,所以上面的解决方案有助于避免这个陷阱。

⚠️ 一件有趣的事

尝试运行下下面的代码:

  1. // Hi,我是个自增数字!
  2. alert( 9999999999999999 ); // shows 10000000000000000

这也存在同样的问题:精度的丢失。这个数字有 64 位,其中 52 位用来存储数字,但这还不够。所以最不重要的数字消失了。

JavaScript 在此类情况下不会触发错误,它尽最大可能地用给定数字的值来表示,但不幸的是,因为存储数字的位数不足够多和大,显示出来的数字可能是一个近似值。

⚠️ 0 和 -0

另一件有趣的事是,零这个数字在内部有两种表现形式:0 和 -0。

这是因为正负号在内部占据 1 位,所以每个数字都有对应的正负形式,包括零。

在大多数情况下,0 和 -0 的区别并不明显,运算符通常把它们看成一样的值来对待。

检查:isFinite 和 isNaN

还记得有两个特殊的值吗?

  • Infinite(和 -Infinite)是表示无穷大(小)。

  • NaN 表示解析出错。

他们都是 number 类型,但不是“正常”的数字,所以有检查他们的特殊方法。

  • isNaN(value) 将给定的参数转换为数值,然后检查是不是 NaN:
  1. alert( isNaN(NaN) ); // true
  2. alert( isNaN("str") ); // true

但我们真的需要这个函数吗?我们不能使用 === 符号来检查 NaN 吗?对不起,不可以,NaN 不等于任何一个值,包括它自己:

  1. alert( NaN === NaN ); // false
  • isFinite(value) 将给定的参数转换为数字,并且如果是常规数字的话,就返回 true。这里的常规数字不包括 NaN/Infinite/-Infinite:
  1. alert( isFinite("15") ); // true
  2. alert( isFinite("str") ); // false, 因为是特殊值: NaN
  3. alert( isFinite(Infinity) ); // false, 因为是特殊值: Infinity

有时可以用 isFinite 来判断,是够一个字符串表示的是常规数字:

  1. let num = +prompt("Enter a number", '');
  2. // will be true unless you enter Infinity, -Infinity or not a number
  3. alert( isFinite(num) );

请注意,在所有的数值函数中,一个空或只有空格的字符串被当作 0 来处理,包括 isFinite 方法也是这样认为的。

tip:使用 Object.is 来比较

有一个特殊的用来比较两个值的内置方法 Object.is:几乎等同于 ===,仅有两点不同(都是边缘场景):

  1. NaN 被认为等于 NaN:Object.is(NaN, NaN) === true,这非常好。

  2. 还有就是 0 个 -0 被认为是不同的两个值:Object.is(0, -0) === false,我们很少关心,但从技术上讲,这两个值是不同的。

其他所有情况,Object.is(a, b) 等同于 a === b。

这种比较方法经常在JavaScript规范中使用。当一个内部算法需要比较两个值是否完全相等时,就使用 Object.is 方法(规范内部内部称为SameValue)。

parseInt 和 parseFloat

数字转换使用加号 + 或者 Number,这是严格意义上的转换。如果传入的值不确实是一个数字,就会失败:

  1. alert( +"100px" ); // NaN

唯一的例外是在字符串的开头或结尾的空格,因为它们被忽略了。

当时在现实生活里,我们经常会遇到带单位的数值场景,像 CSS 里的“100px”或者“12pt”。当然在一些国家,还在数字之后跟货币符号。所以,我们就有了从“19€”取出精确数字的需要了。

这就是有 parseInt 和 parseFloat 函数的原因。

他们从一个字符串里尽可能的“读取”能读取到的数值,一旦遇到的不是数字了,就返回之前的字符串里的所表示的数字,最为结果值。parseInt 返回整数,而 parseFloat 返回的是浮点数。

  1. alert( parseInt('100px') ); // 100
  2. alert( parseFloat('12.5em') ); // 12.5
  3. alert( parseInt('12.3') ); // 12, 仅整数部分返回了
  4. alert( parseFloat('12.3.4') ); // 12.3, 在第二个点处停止阅读

在某些场景下,parseInt/parseFloat 返回 NaN。当无数字可读时,就返回这个值。

  1. alert( parseInt('a123') ); // NaN, the first symbol stops the process

tip: parseInt(str, radix) 的第二个参数

parseInt() 还可以接收可选的第二个参数,指定数字系统的基数。因此 parseInt 还可以解析十六进制、八进制等形式的字符串。

  1. alert( parseInt('0xff', 16) ); // 255
  2. alert( parseInt('ff', 16) ); // 255, 没有 0x 同样也可以正常解析
  3. alert( parseInt('2n9c', 36) ); // 123456

其他数学函数

JavaScript 内置 Math 对象身上有提供一些数学函数和常亮。

举一些例子:

Math.random()

返回 0 到 1 之间的任意一个数(不包括 1)。

  1. alert( Math.random() ); // 0.1234567894322
  2. alert( Math.random() ); // 0.5435252343232
  3. alert( Math.random() ); // ... (any random numbers)

Math.max(a, b, c…) / Math.min(a, b, c…)

返回参数列表总最大的/最小的数字。

  1. alert( Math.max(3, 5, -10, 0, 1) ); // 5
  2. alert( Math.min(1, 2) ); // 1

Math.pow(n, power)

返回指定数字 n 的 power 次幂。

  1. alert( Math.pow(2, 10) ); // 2 in power 10 = 1024

Math 对象上还有更多的函数和常量,包括常用的三角函数,可以在 Math 对象文档 里看到。

总结

书写大数:

  • 在数字之后附加“e”来表示 0 的个数。比如:123e6 就是 123 后面跟 6 个 0。

  • “e”后面跟上负数同样表示 0 的个数,不过是指 1 被除的倍数。比如:123e-6 中的 “e6”表示的是一百万分之一。

不同的数字系统:

  • 可以直接书写数字的十六进制(0x)、八进制(0o)或二进制(0b)形式。

  • parseInt(str, base) 表示以制定基数(2 <= base <= 36)将字符串 str 解析成数字。

  • num.toString() 将一个数字依据指定基数(base)转换成字符串。

将 12pt 或者 100px 这类字符串装成数字:

  • 使用 parseInt/parseFloat 进行“软转换”。

针对分数:

  • 舍入方法:Math.floor,Math.ceil,Math.trunc,Math.round 还有 num.toFixed(percision)。

  • 一定要记住,在处理分数时,会有精度的损失。

更多的数学函数:

  • 必要时查看 Math 对象的文档。这个库很小,但可以满足基本的需要。