第二章:值 - 特殊值
特殊值
在各种类型中散布着一些特殊值,需要 警惕 的 JS 开发者小心,并正确使用。
不是值的值
对于 undefined
类型来说,有且仅有一个值:undefined
。对于 null
类型来说,有且仅有一个值:null
。所以对它们而言,这些文字既是它们的类型也是它们的值。
undefined
和 null
作为“空”值或者“没有”值,经常被认为是可以互换的。另一些开发者偏好于使用微妙的区别将它们区分开。举例来讲:
null
是一个空值undefined
是一个丢失的值
或者:
undefined
还没有值null
曾经有过值但现在没有
不管你选择如何“定义”和使用这两个值,null
是一个特殊的关键字,不是一个标识符,因此你不能将它作为一个变量对待来给它赋值(为什么你要给它赋值呢?!)。然而,undefined
(不幸地)是 一个标识符。噢。
Undefined
在非 strict
模式下,给在全局上提供的 undefined
标识符赋一个值实际上是可能的(虽然这是一个非常不好的做法!):
function foo() {
undefined = 2; // 非常差劲儿的主意!
}
foo();
function foo() {
"use strict";
undefined = 2; // TypeError!
}
foo();
但是,在非 strict
模式和 strict
模式下,你可以创建一个名叫 undefined
局部变量。但这又是一个很差劲儿的主意!
function foo() {
"use strict";
var undefined = 2;
console.log( undefined ); // 2
}
foo();
朋友永远不让朋友覆盖 undefined
。
void
操作符
虽然 undefined
是一个持有内建的值 undefined
的内建标识符(除非被修改 —— 见上面的讨论!),另一个得到这个值的方法是 void
操作符。
表达式 void __
会“躲开”任何值,所以这个表达式的结果总是值 undefined
。它不会修改任何已经存在的值;只是确保不会有值从操作符表达式中返回来。
var a = 42;
console.log( void a, a ); // undefined 42
从惯例上讲(大约是从 C 语言编程中发展而来),要通过使用 void
来独立表现值 undefined
,你可以使用 void 0
(虽然,很明显,void true
或者任何其他的 void
表达式都做同样的事情)。在 void 0
、void 1
和 undefined
之间没有实际上的区别。
但是在几种其他的环境下 void
操作符可以十分有用:如果你需要确保一个表达式没有结果值(即便它有副作用)。
举个例子:
function doSomething() {
// 注意:`APP.ready` 是由我们的应用程序提供的
if (!APP.ready) {
// 稍后再试一次
return void setTimeout( doSomething, 100 );
}
var result;
// 做其他一些事情
return result;
}
// 我们能立即执行吗?
if (doSomething()) {
// 马上处理其他任务
}
这里,setTimeout(..)
函数返回一个数字值(时间间隔定时器的唯一标识符,用于取消它自己),但是我们想 void
它,这样我们函数的返回值不会在 if
语句上给出一个成立的误报。
许多开发者宁愿将这些动作分开,这样的效用相同但不使用 void
操作符:
if (!APP.ready) {
// 稍后再试一次
setTimeout( doSomething, 100 );
return;
}
一般来说,如果有那么一个地方,有一个值存在(来自某个表达式)而你发现这个值如果是 undefined
才有用,就使用 void
操作符。这可能在你的程序中不是非常常见,但如果在一些稀有的情况下你需要它,它就十分有用。
特殊的数字
number
类型包含几种特殊值。我们将会仔细考察每一种。
不是数字的数字
如果你不使用同为 number
(或者可以被翻译为十进制或十六进制的普通 number
的值)的两个操作数进行任何算数操作,那么操作的结果将失败而产生一个不合法的 number
,在这种情况下你将得到 NaN
值。
NaN
在字面上代表“不是一个 number
(Not a Number)”,但是正如我们即将看到的,这种文字描述十分失败而且容易误导人。将 NaN
考虑为“不合法数字”,“失败的数字”,甚至是“坏掉的数字”都要比“不是一个数字”准确得多。
举例来说:
var a = 2 / "foo"; // NaN
typeof a === "number"; // true
换句话说:“‘不是一个数字’的类型是‘数字’”!为这使人糊涂的名字和语义欢呼吧。
NaN
是一种“哨兵值”(一个被赋予了特殊意义的普通的值),它代表 number
集合内的一种特殊的错误情况。这种错误情况实质上是:“我试着进行数学操作但是失败了,而这就是失败的 number
结果。”
那么,如果你有一个值存在某个变量中,而且你想要测试它是否是这个特殊的失败数字 NaN
,你也许认为你可以直接将它与 NaN
本身比较,就像你能对其它的值做的那样,比如 null
和 undefined
。不是这样。
var a = 2 / "foo";
a == NaN; // false
a === NaN; // false
NaN
是一个非常特殊的值,它从来不会等于另一个 NaN
值(也就是,它从来不等于它自己)。实际上,它是唯一一个不具有反射性的值(没有恒等性 x === x
)。所以,NaN !== NaN
。有点奇怪,对吧?
那么,如果不能与 NaN
进行比较(因为这种比较将总是失败),我们该如何测试它呢?
var a = 2 / "foo";
isNaN( a ); // true
够简单的吧?我们使用称为 isNaN(..)
的内建全局工具,它告诉我们这个值是否是 NaN
。问题解决了!
别高兴得太早。
isNaN(..)
工具有一个重大缺陷。它似乎过于按照字面的意思(“不是一个数字”)去理解 NaN
的含义了 —— 它的工作基本上是:“测试这个传进来的东西是否不是一个 number
或者是一个 number
”。但这不是十分准确。
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; // "foo"
window.isNaN( a ); // true
window.isNaN( b ); // true -- 噢!
很明显,"foo"
根本 不是一个 number
,但它也绝不是一个 NaN
值!这个 bug 从最开始的时候就存在于 JS 中了(存在超过了十九年的坑)。
在 ES6 中,终于提供了一个替代它的工具:Number.isNaN(..)
。有一个简单的填补,可以让你即使是在前 ES6 的浏览器中安全地检查 NaN
值:
if (!Number.isNaN) {
Number.isNaN = function(n) {
return (
typeof n === "number" &&
window.isNaN( n )
);
};
}
var a = 2 / "foo";
var b = "foo";
Number.isNaN( a ); // true
Number.isNaN( b ); // false -- 咻!
实际上,通过利用 NaN
与它自己不相等这个特殊的事实,我们可以更简单地实现 Number.isNaN(..)
的填补。在整个语言中 NaN
是唯一一个这样的值;其他的值都总是 等于它自己。
所以:
if (!Number.isNaN) {
Number.isNaN = function(n) {
return n !== n;
};
}
怪吧?但是好用!
不管有意还是无意,在许多真实世界的 JS 程序中 NaN
可能是一个现实的问题。使用 Number.isNaN(..)
(或者它的填补)这样的可靠测试来正确地识别它们是一个非常好的主意。
如果你正在程序中仅使用 isNaN(..)
,悲惨的现实是你的程序 有 bug,即便是你还没有被它咬到!
无穷
来自于像 C 这样的传统编译型语言的开发者,可能习惯于看到编译器错误或者是运行时异常,比如对这样一个操作给出的“除数为 0”:
var a = 1 / 0;
然而在 JS 中,这个操作是明确定义的,而且它的结果是值 Infinity
(也就是 Number.POSITIVE_INFINITY
)。意料之中的是:
var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity
如你所见,-Infinity
(也就是 Number.NEGATIVE_INFINITY
)是从任一个被除数为负(不是两个都是负数!)的除 0 操作得来的。
JS 使用有限的数字表现形式(IEEE 754 浮点,我们早先讨论过),所以和单纯的数学相比,它看起来甚至在做加法和减法这样的操作时都有可能溢出,这样的情况下你将会得到 Infinity
或 -Infinity
。
例如:
var a = Number.MAX_VALUE; // 1.7976931348623157e+308
a + a; // Infinity
a + Math.pow( 2, 970 ); // Infinity
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
指定,负的零还可以从特定的数学操作中得出。比如:
var a = 0 / -3; // -0
var b = 0 * -3; // -0
加法和减法无法得出负零。
在开发者控制台中考察一个负的零,经常显示为 -0
,然而直到最近这才是一个常见情况,所以一些你可能遇到的老版本浏览器也许依然将它报告为 0
。
但是根据语言规范,如果你试着将一个负零转换为字符串,它将总会被报告为 "0"
。
var a = 0 / -3;
// 至少(有些浏览器)控制台是对的
a; // -0
// 但是语言规范坚持要向你撒谎!
a.toString(); // "0"
a + ""; // "0"
String( a ); // "0"
// 奇怪的是,就连 JSON 也加入了骗局之中
JSON.stringify( a ); // "0"
有趣的是,反向操作(从 string
到 number
)不会撒谎:
+"-0"; // -0
Number( "-0" ); // -0
JSON.parse( "-0" ); // -0
警告: 当你观察的时候,JSON.stringify( -0 )
产生 "0"
显得特别奇怪,因为它与反向操作不符:JSON.parse( "-0" )
将像你期望地那样报告-0
。
除了一个负零的字符串化会欺骗性地隐藏它实际的值外,比较操作符也被设定为(有意地) 要说谎。
var a = 0;
var b = 0 / -3;
a == b; // true
-0 == 0; // true
a === b; // true
-0 === 0; // true
0 > -0; // false
a > b; // false
很明显,如果你想在你的代码中区分 -0
和 0
,你就不能仅依靠开发者控制台的输出,你必须更聪明一些:
function isNegZero(n) {
n = Number( n );
return (n === 0) && (1 / n === -Infinity);
}
isNegZero( -0 ); // true
isNegZero( 0 / -3 ); // true
isNegZero( 0 ); // false
那么,除了学院派的细节以外,我们为什么需要一个负零呢?
在一些应用程序中,开发者使用值的大小来表示一部分信息(比如动画中每一帧的速度),而这个 number
的符号来表示另一部分信息(比如移动的方向)。
在这些应用程序中,举例来说,如果一个变量的值变成了 0,而它丢失了符号,那么你就丢失了它是从哪个方向移动到 0 的信息。保留零的符号避免了潜在的意外信息丢失。
特殊等价
正如我们上面看到的,当使用等价性比较时,值 NaN
和值 -0
拥有特殊的行为。NaN
永远不会和自己相等,所以你不得不使用 ES6 的 Number.isNaN(..)
(或者它的填补)。相似地,-0
撒谎并假装它和普通的正零相等(即使使用 ===
严格等价 —— 见第四章),所以你不得不使用我们上面建议的某些 isNegZero(..)
黑科技工具。
在 ES6 中,有一个新工具可以用于测试两个值的绝对等价性,而没有任何这些例外。它称为 Object.is(..)
:
var a = 2 / "foo";
var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false
对于前 ES6 环境,这是一个相当简单的 Object.is(..)
填补:
if (!Object.is) {
Object.is = function(v1, v2) {
// 测试 `-0`
if (v1 === 0 && v2 === 0) {
return 1 / v1 === 1 / v2;
}
// 测试 `NaN`
if (v1 !== v1) {
return v2 !== v2;
}
// 其他情况
return v1 === v2;
};
}
Object.is(..)
可能不应当用于那些 ==
或 ===
已知 安全 的情况(见第四章“强制转换”),因为这些操作符可能高效得多,并且更惯用/常见。Object.is(..)
很大程度上是为这些特殊的等价情况准备的。