与其他的语言相比,JavaScript 中 undefined 的概念是有些令人困惑的。特别是试图去理解 ReferenceError(“x is not defined”)以及如何针对它们写出优雅的代码是很令人沮丧的。
本文是我试图把这件事情弄清楚的一些尝试。如果你还不熟悉 JavaScript 中变量和属性的区别(包括内部的 VariableObject),那么最好先去阅读一下我的上一篇文章。
什么是 undefined?
在 JavaScript 中有 Undefined (type)、undefined (value) 和 undefined (variable)。
Undefined (type) 是 JavaScript 的内置类型。
undefined (value) 是 Undefined 类型的唯一的值。任何未被赋值的属性都被假定为 undefined(ECMA 4.3.9 和 4.3.10)。没有 return 语句的函数,或者 return 空的函数将返回 undefined。函数中没有被定义的参数的值也被认为是 undefined。
undefined (variable) 是一个初始值为 undefined (value) 的全局属性,因为它是一个全局属性,我们还可以将其作为变量访问。为了保持一致性,我在本文中统一称它为变量。
从 ECMA 3 开始,它可以被重新赋值:
毋庸置疑,给 undefined 变量重新赋值是非常不好的做法。事实上,ECMA 5 不允许这样做(不过,在当前的浏览器中,只有 Safari 强制执行了)。
然后是 null?
是的,一般都很好理解,但是还需要重申的是:undefined 与 null 不同,null 表示有意的缺少值的原始值。undefined 和 null 唯一的相似之处是,它们都为 false。
所以,什么是 ReferenceError(引用错误)?
ReferenceError 说明检测到了一个无效的引用值。(ECMA 5 15.11.6.3)
在实际项目中,这意味着当 JavaScript 试图获取一个不可被解析的引用时,会抛出 ReferenceError。(还有一些其他的情况会抛出 ReferenceError,尤其是在 ECMA 5 严格模式下运行的时候。如果你有兴趣的话,可以看本文末尾的阅读列表。)
需要注意不同浏览器发出的消息语法是如何变化的,正如我们将看到的,这些信息没有一个是特别有启发性的:
仍然不清楚“无法解析的引用(unresolvable reference)”?
在 ECMA 术语中,引用由基值(base value)和引用名(reference name)构成(ECMA 5 8.7 - 我再次忽略了严格模式。还要注意,ECMA 3 的术语略有不同,但实际意义是相同的)。
如果引用是属性,那么基值和引用名位于 . 的两侧(或第一个括号或其他):
对于变量引用,基值是当前执行上下文的 VariableObject。全局上下文的 VariableObject 是全局对象本身(浏览器中的 window)。每个函数上下文都有一个抽象的变量对象,称为 ActivationObject。
var foo; //base value = window, reference name = foo
function a() {
var b; base value = <code>ActivationObject</code>, reference name = b
}
如果基值是 undefined,则认为引用是无法被解析的。
因此,如果在 . 之前的变量值为 undefined,那么属性引用是不可被解析的。下面的示例本会抛出一个 ReferenceError,但实际上它不会,因为 TypeError 会先被抛出。这是因为属性的基值受 CheckObjectCoercible (ECMA 5 9.10 到 11.2.1)的影响,在它尝试将 Undefined 类型转换为 Object 的时候会抛出 TypeError。(感谢 kangax 在 twitter 上提前发布的消息)
变量引用永远会被解析,因为 var 关键字确保 VariableObject 总是被赋给基值。
根据定义,既不是属性也不是变量的引用是不可解析的,并且会抛出一个 ReferenceError:
上面的 JavaScript 中没有看到显式的基值,因此会查找 VariableObject 来引用名称为 foo 的属性。确定 foo 没有基值,然后抛出 ReferenceError。
但是 foo 不是一个未声明的变量吗?
技术上不是的。虽然我们有时会发现 “undeclared variable” 是一个错误诊断时有用的术语,但实际上,在变量被声明之前不是变量。
那么隐式全局变量呢?
的确,从未被 var 关键字声明过的标识符将被创建为全局变量 —— 但只有当它们被赋值时才会这样。
当然,这很烦人。如果 JavaScript 在遇到无法解析的引用时始终抛出 ReferenceErrors 那就更好了(实际上这是它在 ECMA 严格模式下所做的)。
什么时候需要针对 ReferenceError 进行编码?
如果你的代码写得够好的话,其实很少需要这样做。我们已经看到,在典型的用法中,只有一种方法可以获得不可解析的引用:使用既不是属性也不是变量的仅在语法上正确的引用。在大多数情况下,确保记住 var 关键字可以避免这种情况。只有在引用只存在于某些浏览器或第三方代码中的变量时,才会出现运行时异常。
一个很好的例子是 console。在 Webkit 浏览器中,console 是内置的,console 的属性总是可用的。然而 firefox 中的 console 依赖于安装和打开Firebug(或其他附加组件)。IE7 没有 console,IE8 有 console,但 console 属性只在 IE 开发工具启动时存在。显然 Opera 有 console,但我从来没有使用过。
结论是,下面的代码片段在浏览器中运行时很可能会抛出 ReferenceError:
console.log(new Date());
如何对可能不存在的变量进行编码?
检查一个不可解析的引用而且不抛出 ReferenceError 的一种方法是使用 typeof 关键字。
if (typeof console != "undefined") {
console.log(new Date());
}
然而,这在我看来总是很繁琐的,更不用说可疑的了(它不是引用名称是 undefined,而是基值为 undefined)。但是无论如何,我更喜欢保留 typeof 来进行类型检查。
幸运的是,还有另一种方法:我们已经知道,如果 undefined 属性的基值被定义,那么它就不会抛出 ReferenceError —— 而且由于 console 属于全局对象,我们就可以这样做:
window.console && console.log(new Date());
实际上,只需要检查全局上下文中是否存在变量(函数中存在其他执行上下文,而且你可以控制自己的函数中存在哪些变量)。所以,理论上你应该能够避免使用 typeof 来检查引用错误。