第二章:值 - Numbers

优质
小牛编辑
126浏览
2023-12-01

Number

JavaScript 只有一种数字类型:number。这种类型包含“整数”值和小数值。我说“整数”时加了引号,因为 JS 的一个长久以来为人诟病的原因是,和其他语言不同,JS 没有真正的整数。这可能在未来某个时候会改变,但是目前,我们只有 number 可用。

所以,在 JS 中,一个“整数”只是一个没有小数部分的小数值。也就是说,42.042 一样是“整数”。

像大多数现代计算机语言,以及几乎所有的脚本语言一样,JavaScript 的 number 的实现基于“IEEE 754”标准,通常被称为“浮点”。JavaScript 明确地使用了这个标准的“双精度”(也就是“64位二进制”)格式。

在网络上有许多了不起的文章都在介绍二进制浮点数如何在内存中存储的细节,以及选择这些做法的意义。因为对于理解如何在 JS 中正确使用 number 来说,理解内存中的位模式不是必须的,所以我们将这个话题作为练习留给那些想要进一步挖掘 IEEE 754 的细节的读者。

数字的语法

在 JavaScript 中字面数字一般用十进制小数表达。例如:

  1. var a = 42;
  2. var b = 42.3;

小数的整数部分如果是 0,是可选的:

  1. var a = 0.42;
  2. var b = .42;

相似地,一个小数在 . 之后的小数部分如果是 0,是可选的:

  1. var a = 42.0;
  2. var b = 42.;

警告: 42. 是极不常见的,如果你正在努力避免别人阅读你的代码时感到困惑,它可能不是一个好主意。但不管怎样,它是合法的。

默认情况下,大多数 number 将会以十进制小数的形式输出,并去掉末尾小数部分的 0。所以:

  1. var a = 42.300;
  2. var b = 42.0;
  3. a; // 42.3
  4. b; // 42

非常大或非常小的 number 将默认以指数形式输出,与 toExponential() 方法的输出一样,比如:

  1. var a = 5E10;
  2. a; // 50000000000
  3. a.toExponential(); // "5e+10"
  4. var b = a * a;
  5. b; // 2.5e+21
  6. var c = 1 / a;
  7. c; // 2e-11

因为 number 值可以用 Number 对象包装器封装(见第三章),所以 number 值可以访问内建在 Number.prototype 上的方法(见第三章)。举个例子,toFixed(..) 方法允许你指定一个值在被表示时,带有多少位小数:

  1. var a = 42.59;
  2. a.toFixed( 0 ); // "43"
  3. a.toFixed( 1 ); // "42.6"
  4. a.toFixed( 2 ); // "42.59"
  5. a.toFixed( 3 ); // "42.590"
  6. a.toFixed( 4 ); // "42.5900"

要注意的是,它的输出实际上是一个 numberstring 表现形式,而且如果你指定的位数多于值持有的小数位数时,会在右侧补 0

toPrecision(..) 很相似,但它指定的是有多少 有效数字 用来表示这个值:

  1. var a = 42.59;
  2. a.toPrecision( 1 ); // "4e+1"
  3. a.toPrecision( 2 ); // "43"
  4. a.toPrecision( 3 ); // "42.6"
  5. a.toPrecision( 4 ); // "42.59"
  6. a.toPrecision( 5 ); // "42.590"
  7. a.toPrecision( 6 ); // "42.5900"

你不必非得使用持有这个值的变量来访问这些方法;你可以直接在 number 的字面上访问这些方法。但你不得不小心 . 操作符。因为 . 是一个合法数字字符,如果可能的话,它会首先被翻译为 number 字面的一部分,而不是被翻译为属性访问操作符。

  1. // 不合法的语法:
  2. 42.toFixed( 3 ); // SyntaxError
  3. // 这些都是合法的:
  4. (42).toFixed( 3 ); // "42.000"
  5. 0.42.toFixed( 3 ); // "0.420"
  6. 42..toFixed( 3 ); // "42.000"

42.toFixed(3) 是不合法的语法,因为 . 作为 42. 字面(这是合法的 — 参见上面的讨论!)的一部分被吞掉了,因此没有 . 属性操作符来表示 .toFixed 访问。

42..toFixed(3) 可以工作,因为第一个 .number 的一部分,而第二个 . 是属性操作符。但它可能看起来很古怪,而且确实在实际的 JavaScript 代码中很少会看到这样的东西。实际上,在任何基本类型上直接访问方法是十分不常见的。但是不常见并不意味着 或者

注意: 有一些库扩展了内建的 Number.prototype(见第三章),使用 number 或在 number 上提供了额外的操作,所以在这些情况下,像使用 10..makeItRain() 来设定一个十秒钟的下钱雨的动画,或者其他诸如此类的傻事是完全合法的。

在技术上讲,这也是合法的(注意那个空格):

  1. 42 .toFixed(3); // "42.000"

但是,尤其是对 number 字面量来说,这是特别使人糊涂的代码风格,而且除了使其他开发者(和未来的你)糊涂以外没有任何用处。避免它。

number 还可以使用科学计数法的形式指定,这在表示很大的 number 时很常见,比如:

  1. var onethousand = 1E3; // 代表 1 * 10^3
  2. var onemilliononehundredthousand = 1.1E6; // 代表 1.1 * 10^6

number 字面量还可以使用其他进制表达,比如二进制,八进制,和十六进制。

这些格式是可以在当前版本的 JavaScript 中使用的:

  1. 0xf3; // 十六进制的: 243
  2. 0Xf3; // 同上
  3. 0363; // 八进制的: 243

注意: 从 ES6 + strict 模式开始,不再允许 0363 这样的的八进制形式(新的形式参见后面的讨论)。0363 在非 strict 模式下依然是允许的,但是不管怎样你应当停止使用它,来拥抱未来(而且因为你现在应当在使用 strict 模式了!)。

至于 ES6,下面的新形式也是合法的:

  1. 0o363; // 八进制的: 243
  2. 0O363; // 同上
  3. 0b11110011; // 二进制的: 243
  4. 0B11110011; // 同上

请为你的开发者同胞们做件好事:绝不要使用 0O363 形式。把 0 放在大写的 O 旁边就是在制造困惑。保持使用小写的谓词 0x0b、和0o

小数值

使用二进制浮点数的最出名(臭名昭著)的副作用是(记住,这是对 所有 使用 IEEE 754 的语言都成立的 —— 不是许多人认为/假装 在 JavaScript 中存在的问题):

  1. 0.1 + 0.2 === 0.3; // false

从数学的意义上,我们知道这个语句应当为 true。为什么它是 false

简单地说,0.10.2 的二进制表示形式是不精确的,所以它们相加时,结果不是精确地 0.3。而是 非常 接近的值:0.30000000000000004,但是如果你的比较失败了,“接近”是无关紧要的。

注意: JavaScript 应当切换到可以精确表达所有值的一个不同的 number 实现吗?有些人认为应该。多年以来有许多选项出现过。但是没有一个被采纳,而且也许永远也不会。它看起来就像挥挥手然后说“已经改好那个 bug 了!”那么简单,但根本不是那么回事儿。如果真有这么简单,它绝对在很久以前就被改掉了。

现在的问题是,如果一些 number 不能被 信任 为精确的,这不是意味着我们根本不能使用 number 吗? 当然不是。

在一些应用程序中你需要多加小心,特别是在对付小数的时候。还有许多(也许是大多数?)应用程序只处理整数,而且,最大只处理到几百万到几万亿。这些应用程序使用 JS 中的数字操作是,而且将总是,非常安全 的。

要是我们 确实 需要比较两个 number,就像 0.1 + 0.20.3,而且知道这个简单的相等测试将会失败呢?

可以接受的最常见的做法是使用一个很小的“错误舍入”值作为比较的 容差。这个很小的值经常被称为“机械极小值(machine epsilon)”,对于 JavaScript 来说这种 number 通常为 2^-522.220446049250313e-16)。

在 ES6 中,使用这个容差值预定义了 Number.EPSILON,所以你将会想要使用它,你也可以在前 ES6 中安全地填补这个定义:

  1. if (!Number.EPSILON) {
  2. Number.EPSILON = Math.pow(2,-52);
  3. }

我们可以使用这个 Number.EPSILON 来比较两个 number 的“等价性”(带有错误舍入的容差):

  1. function numbersCloseEnoughToEqual(n1,n2) {
  2. return Math.abs( n1 - n2 ) < Number.EPSILON;
  3. }
  4. var a = 0.1 + 0.2;
  5. var b = 0.3;
  6. numbersCloseEnoughToEqual( a, b ); // true
  7. numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false

可以被表示的最大的浮点值大概是 1.798e+308(它真的非常,非常,非常大!),它为你预定义为 Number.MAX_VALUE。在极小的一端,Number.MIN_VALUE 大概是 5e-324,它不是负数但是非常接近于0!

安全整数范围

由于 number 的表示方式,对完全是 number 的“整数”而言有一个“安全”的值的范围,而且它要比 Number.MAX_VALUE 小得多。

可以“安全地”被表示的最大整数(也就是说,可以保证被表示的值是实际可以无误地表示的)是2^53 - 1,也就是9007199254740991,如果你插入一些数字分隔符,可以看到它刚好超过9万亿。所以对于number能表示的上限来说它确实是够TM大的。

在ES6中这个值实际上是自动预定义的,它是Number.MAX_SAFE_INTEGER。意料之中的是,还有一个最小值,-9007199254740991,它在ES6中定义为Number.MIN_SAFE_INTEGER

JS 程序面临处理这样大的数字的主要情况是,处理数据库中的64位 ID 等等。64位数字不能使用 number 类型准确表达,所以在 JavaScript 中必须使用 string 表现形式存储(和传递)。

谢天谢地,在这样的大 ID number 值上的数字操作(除了比较,它使用 string 也没问题)并不很常见。但是如果你 确实 需要在这些非常大的值上实施数学操作,目前来讲你需要使用一个 大数字 工具。在未来版本的 JavaScript 中,大数字也许会得到官方支持。

测试整数

测试一个值是否是整数,你可以使用 ES6 定义的 Number.isInteger(..)

  1. Number.isInteger( 42 ); // true
  2. Number.isInteger( 42.000 ); // true
  3. Number.isInteger( 42.3 ); // false

可以为前 ES6 填补 Number.isInteger(..)

  1. if (!Number.isInteger) {
  2. Number.isInteger = function(num) {
  3. return typeof num == "number" && num % 1 == 0;
  4. };
  5. }

要测试一个值是否是 安全整数,使用 ES6 定义的 Number.isSafeInteger(..)

  1. Number.isSafeInteger( Number.MAX_SAFE_INTEGER ); // true
  2. Number.isSafeInteger( Math.pow( 2, 53 ) ); // false
  3. Number.isSafeInteger( Math.pow( 2, 53 ) - 1 ); // true

可以为前 ES6 浏览器填补 Number.isSafeInteger(..)

  1. if (!Number.isSafeInteger) {
  2. Number.isSafeInteger = function(num) {
  3. return Number.isInteger( num ) &&
  4. Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
  5. };
  6. }

32位(有符号)整数

虽然整数可以安全地最大达到约九万亿(53比特),但有一些数字操作(比如位操作符)是仅仅为32位 number 定义的,所以对于被这样使用的 number 来说,“安全范围”一定会小得多。

这个范围是从 Math.pow(-2,31)-2147483648,大约-21亿)到 Math.pow(2,31)-12147483647,大约+21亿)。

要强制 a 中的 number 值是32位有符号整数,使用 a | 0。这可以工作是因为 | 位操作符仅仅对32位值起作用(意味着它可以只关注32位,而其他的位将被丢掉)。而且,和 0 进行“或”的位操作实质上是什么也不做。

注意: 特定的特殊值(我们将在下一节讨论),比如 NaNInfinity 不是“32位安全”的,当这些值被传入位操作符时将会通过一个抽象操作 ToInt32(见第四章)并为了位操作而简单地变成 +0 值。