第二章:值 - 特殊值

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

特殊值

在各种类型中散布着一些特殊值,需要 警惕 的 JS 开发者小心,并正确使用。

不是值的值

对于 undefined 类型来说,有且仅有一个值:undefined。对于 null 类型来说,有且仅有一个值:null。所以对它们而言,这些文字既是它们的类型也是它们的值。

undefinednull 作为“空”值或者“没有”值,经常被认为是可以互换的。另一些开发者偏好于使用微妙的区别将它们区分开。举例来讲:

  • null 是一个空值
  • undefined 是一个丢失的值

或者:

  • undefined 还没有值
  • null 曾经有过值但现在没有

不管你选择如何“定义”和使用这两个值,null 是一个特殊的关键字,不是一个标识符,因此你不能将它作为一个变量对待来给它赋值(为什么你要给它赋值呢?!)。然而,undefined(不幸地) 一个标识符。噢。

Undefined

在非 strict 模式下,给在全局上提供的 undefined 标识符赋一个值实际上是可能的(虽然这是一个非常不好的做法!):

  1. function foo() {
  2. undefined = 2; // 非常差劲儿的主意!
  3. }
  4. foo();
  1. function foo() {
  2. "use strict";
  3. undefined = 2; // TypeError!
  4. }
  5. foo();

但是,在非 strict 模式和 strict 模式下,你可以创建一个名叫 undefined 局部变量。但这又是一个很差劲儿的主意!

  1. function foo() {
  2. "use strict";
  3. var undefined = 2;
  4. console.log( undefined ); // 2
  5. }
  6. foo();

朋友永远不让朋友覆盖 undefined

void 操作符

虽然 undefined 是一个持有内建的值 undefined 的内建标识符(除非被修改 —— 见上面的讨论!),另一个得到这个值的方法是 void 操作符。

表达式 void __ 会“躲开”任何值,所以这个表达式的结果总是值 undefined。它不会修改任何已经存在的值;只是确保不会有值从操作符表达式中返回来。

  1. var a = 42;
  2. console.log( void a, a ); // undefined 42

从惯例上讲(大约是从 C 语言编程中发展而来),要通过使用 void 来独立表现值 undefined,你可以使用 void 0(虽然,很明显,void true 或者任何其他的 void 表达式都做同样的事情)。在 void 0void 1undefined 之间没有实际上的区别。

但是在几种其他的环境下 void 操作符可以十分有用:如果你需要确保一个表达式没有结果值(即便它有副作用)。

举个例子:

  1. function doSomething() {
  2. // 注意:`APP.ready` 是由我们的应用程序提供的
  3. if (!APP.ready) {
  4. // 稍后再试一次
  5. return void setTimeout( doSomething, 100 );
  6. }
  7. var result;
  8. // 做其他一些事情
  9. return result;
  10. }
  11. // 我们能立即执行吗?
  12. if (doSomething()) {
  13. // 马上处理其他任务
  14. }

这里,setTimeout(..) 函数返回一个数字值(时间间隔定时器的唯一标识符,用于取消它自己),但是我们想 void 它,这样我们函数的返回值不会在 if 语句上给出一个成立的误报。

许多开发者宁愿将这些动作分开,这样的效用相同但不使用 void 操作符:

  1. if (!APP.ready) {
  2. // 稍后再试一次
  3. setTimeout( doSomething, 100 );
  4. return;
  5. }

一般来说,如果有那么一个地方,有一个值存在(来自某个表达式)而你发现这个值如果是 undefined 才有用,就使用 void 操作符。这可能在你的程序中不是非常常见,但如果在一些稀有的情况下你需要它,它就十分有用。

特殊的数字

number 类型包含几种特殊值。我们将会仔细考察每一种。

不是数字的数字

如果你不使用同为 number(或者可以被翻译为十进制或十六进制的普通 number 的值)的两个操作数进行任何算数操作,那么操作的结果将失败而产生一个不合法的 number,在这种情况下你将得到 NaN 值。

NaN 在字面上代表“不是一个 number(Not a Number)”,但是正如我们即将看到的,这种文字描述十分失败而且容易误导人。将 NaN 考虑为“不合法数字”,“失败的数字”,甚至是“坏掉的数字”都要比“不是一个数字”准确得多。

举例来说:

  1. var a = 2 / "foo"; // NaN
  2. typeof a === "number"; // true

换句话说:“‘不是一个数字’的类型是‘数字’”!为这使人糊涂的名字和语义欢呼吧。

NaN 是一种“哨兵值”(一个被赋予了特殊意义的普通的值),它代表 number 集合内的一种特殊的错误情况。这种错误情况实质上是:“我试着进行数学操作但是失败了,而这就是失败的 number 结果。”

那么,如果你有一个值存在某个变量中,而且你想要测试它是否是这个特殊的失败数字 NaN,你也许认为你可以直接将它与 NaN 本身比较,就像你能对其它的值做的那样,比如 nullundefined。不是这样。

  1. var a = 2 / "foo";
  2. a == NaN; // false
  3. a === NaN; // false

NaN 是一个非常特殊的值,它从来不会等于另一个 NaN 值(也就是,它从来不等于它自己)。实际上,它是唯一一个不具有反射性的值(没有恒等性 x === x)。所以,NaN !== NaN。有点奇怪,对吧?

那么,如果不能与 NaN 进行比较(因为这种比较将总是失败),我们该如何测试它呢?

  1. var a = 2 / "foo";
  2. isNaN( a ); // true

够简单的吧?我们使用称为 isNaN(..) 的内建全局工具,它告诉我们这个值是否是 NaN。问题解决了!

别高兴得太早。

isNaN(..) 工具有一个重大缺陷。它似乎过于按照字面的意思(“不是一个数字”)去理解 NaN 的含义了 —— 它的工作基本上是:“测试这个传进来的东西是否不是一个 number 或者是一个 number”。但这不是十分准确。

  1. var a = 2 / "foo";
  2. var b = "foo";
  3. a; // NaN
  4. b; // "foo"
  5. window.isNaN( a ); // true
  6. window.isNaN( b ); // true -- 噢!

很明显,"foo" 根本 不是一个 number,但它也绝不是一个 NaN 值!这个 bug 从最开始的时候就存在于 JS 中了(存在超过了十九年的坑)。

在 ES6 中,终于提供了一个替代它的工具:Number.isNaN(..)。有一个简单的填补,可以让你即使是在前 ES6 的浏览器中安全地检查 NaN 值:

  1. if (!Number.isNaN) {
  2. Number.isNaN = function(n) {
  3. return (
  4. typeof n === "number" &&
  5. window.isNaN( n )
  6. );
  7. };
  8. }
  9. var a = 2 / "foo";
  10. var b = "foo";
  11. Number.isNaN( a ); // true
  12. Number.isNaN( b ); // false -- 咻!

实际上,通过利用 NaN 与它自己不相等这个特殊的事实,我们可以更简单地实现 Number.isNaN(..) 的填补。在整个语言中 NaN 是唯一一个这样的值;其他的值都总是 等于它自己

所以:

  1. if (!Number.isNaN) {
  2. Number.isNaN = function(n) {
  3. return n !== n;
  4. };
  5. }

怪吧?但是好用!

不管有意还是无意,在许多真实世界的 JS 程序中 NaN 可能是一个现实的问题。使用 Number.isNaN(..)(或者它的填补)这样的可靠测试来正确地识别它们是一个非常好的主意。

如果你正在程序中仅使用 isNaN(..),悲惨的现实是你的程序 有 bug,即便是你还没有被它咬到!

无穷

来自于像 C 这样的传统编译型语言的开发者,可能习惯于看到编译器错误或者是运行时异常,比如对这样一个操作给出的“除数为 0”:

  1. var a = 1 / 0;

然而在 JS 中,这个操作是明确定义的,而且它的结果是值 Infinity(也就是 Number.POSITIVE_INFINITY)。意料之中的是:

  1. var a = 1 / 0; // Infinity
  2. var b = -1 / 0; // -Infinity

如你所见,-Infinity(也就是 Number.NEGATIVE_INFINITY)是从任一个被除数为负(不是两个都是负数!)的除 0 操作得来的。

JS 使用有限的数字表现形式(IEEE 754 浮点,我们早先讨论过),所以和单纯的数学相比,它看起来甚至在做加法和减法这样的操作时都有可能溢出,这样的情况下你将会得到 Infinity-Infinity

例如:

  1. var a = Number.MAX_VALUE; // 1.7976931348623157e+308
  2. a + a; // Infinity
  3. a + Math.pow( 2, 970 ); // Infinity
  4. a + Math.pow( 2, 969 ); // 1.7976931348623157e+308

根据语言规范,如果一个像加法这样的操作得到一个太大而不能表示的值,IEEE 754 “就近舍入”模式将会指明结果应该是什么。所以粗略的意义上,Number.MAX_VALUE + Math.pow( 2, 969 ) 比起 Infinity 更接近于 Number.MAX_VALUE,所以它“向下舍入”,而 Number.MAX_VALUE + Math.pow( 2, 970 ) 距离 Infinity 更近,所以它“向上舍入”。

如果你对此考虑的太多,它会使你头疼的。所以别想了。我是认真的,停!

一旦你溢出了任意一个 无限值,那么,就没有回头路了。换句最有诗意的话说,你可以从有限迈向无限,但不能从无限回归有限。

“无限除以无限等于什么”,这简直是一个哲学问题。我们幼稚的大脑可能会说“1”或“无限”。事实表明它们都不对。在数学上和在 JavaScript 中,Infinity / Infinity 不是一个有定义的操作。在 JS 中,它的结果为 NaN

一个有限的正 number 除以 Infinity 呢?简单!0。那一个有限的负 number 处理 Infinity 呢?接着往下读!

虽然这可能使有数学头脑的读者困惑,但 JavaScript 拥有普通的零 0(也称为正零 +0 一个负零 -0。在我们讲解为什么 -0 存在之前,我们应该考察 JS 如何处理它,因为它可能十分令人困惑。

除了使用字面量 -0 指定,负的零还可以从特定的数学操作中得出。比如:

  1. var a = 0 / -3; // -0
  2. var b = 0 * -3; // -0

加法和减法无法得出负零。

在开发者控制台中考察一个负的零,经常显示为 -0,然而直到最近这才是一个常见情况,所以一些你可能遇到的老版本浏览器也许依然将它报告为 0

但是根据语言规范,如果你试着将一个负零转换为字符串,它将总会被报告为 "0"

  1. var a = 0 / -3;
  2. // 至少(有些浏览器)控制台是对的
  3. a; // -0
  4. // 但是语言规范坚持要向你撒谎!
  5. a.toString(); // "0"
  6. a + ""; // "0"
  7. String( a ); // "0"
  8. // 奇怪的是,就连 JSON 也加入了骗局之中
  9. JSON.stringify( a ); // "0"

有趣的是,反向操作(从 stringnumber)不会撒谎:

  1. +"-0"; // -0
  2. Number( "-0" ); // -0
  3. JSON.parse( "-0" ); // -0

警告: 当你观察的时候,JSON.stringify( -0 ) 产生 "0" 显得特别奇怪,因为它与反向操作不符:JSON.parse( "-0" ) 将像你期望地那样报告-0

除了一个负零的字符串化会欺骗性地隐藏它实际的值外,比较操作符也被设定为(有意地) 要说谎

  1. var a = 0;
  2. var b = 0 / -3;
  3. a == b; // true
  4. -0 == 0; // true
  5. a === b; // true
  6. -0 === 0; // true
  7. 0 > -0; // false
  8. a > b; // false

很明显,如果你想在你的代码中区分 -00,你就不能仅依靠开发者控制台的输出,你必须更聪明一些:

  1. function isNegZero(n) {
  2. n = Number( n );
  3. return (n === 0) && (1 / n === -Infinity);
  4. }
  5. isNegZero( -0 ); // true
  6. isNegZero( 0 / -3 ); // true
  7. isNegZero( 0 ); // false

那么,除了学院派的细节以外,我们为什么需要一个负零呢?

在一些应用程序中,开发者使用值的大小来表示一部分信息(比如动画中每一帧的速度),而这个 number 的符号来表示另一部分信息(比如移动的方向)。

在这些应用程序中,举例来说,如果一个变量的值变成了 0,而它丢失了符号,那么你就丢失了它是从哪个方向移动到 0 的信息。保留零的符号避免了潜在的意外信息丢失。

特殊等价

正如我们上面看到的,当使用等价性比较时,值 NaN 和值 -0 拥有特殊的行为。NaN 永远不会和自己相等,所以你不得不使用 ES6 的 Number.isNaN(..)(或者它的填补)。相似地,-0 撒谎并假装它和普通的正零相等(即使使用 === 严格等价 —— 见第四章),所以你不得不使用我们上面建议的某些 isNegZero(..) 黑科技工具。

在 ES6 中,有一个新工具可以用于测试两个值的绝对等价性,而没有任何这些例外。它称为 Object.is(..):

  1. var a = 2 / "foo";
  2. var b = -3 * 0;
  3. Object.is( a, NaN ); // true
  4. Object.is( b, -0 ); // true
  5. Object.is( b, 0 ); // false

对于前 ES6 环境,这是一个相当简单的 Object.is(..) 填补:

  1. if (!Object.is) {
  2. Object.is = function(v1, v2) {
  3. // 测试 `-0`
  4. if (v1 === 0 && v2 === 0) {
  5. return 1 / v1 === 1 / v2;
  6. }
  7. // 测试 `NaN`
  8. if (v1 !== v1) {
  9. return v2 !== v2;
  10. }
  11. // 其他情况
  12. return v1 === v2;
  13. };
  14. }

Object.is(..) 可能不应当用于那些 ===== 已知 安全 的情况(见第四章“强制转换”),因为这些操作符可能高效得多,并且更惯用/常见。Object.is(..) 很大程度上是为这些特殊的等价情况准备的。