[译] 理解 JavaScript 中的 undefined
与其他的语言相比,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。
var a;
typeof a; //"undefined"
window.b;
typeof window.b; //"undefined"
var c = (function() {})();
typeof c; //"undefined"
var d = (function(e) {return e})();
typeof d; //"undefined"
undefined (variable) 是一个初始值为 undefined (value) 的全局属性,因为它是一个全局属性,我们还可以将其作为变量访问。为了保持一致性,我在本文中统一称它为变量。
typeof undefined; //"undefined"
var f = 2;
f = undefined; //re-assigning to undefined (variable)
typeof f; //"undefined"
从 ECMA 3 开始,它可以被重新赋值:
undefined = "washing machine"; //把一个字符串赋值给 undefined (变量)
typeof undefined //"string"
f = undefined;
typeof f; //"string"
f; //"washing machine"
毋庸置疑,给 undefined 变量重新赋值是非常不好的做法。事实上,ECMA 5 不允许这样做(不过,在当前的浏览器中,只有 Safari 强制执行了)。
然后是 null?
是的,一般都很好理解,但是还需要重申的是:undefined
与 null
不同,null
表示有意的缺少值的原始值。undefined
和 null
唯一的相似之处是,它们都为 false。
所以,什么是 ReferenceError(引用错误)?
ReferenceError 说明检测到了一个无效的引用值。(ECMA 5 15.11.6.3)
在实际项目中,这意味着当 JavaScript 试图获取一个不可被解析的引用时,会抛出 ReferenceError。(还有一些其他的情况会抛出 ReferenceError,尤其是在 ECMA 5 严格模式下运行的时候。如果你有兴趣的话,可以看本文末尾的阅读列表。)
需要注意不同浏览器发出的消息语法是如何变化的,正如我们将看到的,这些信息没有一个是特别有启发性的:
alert(foo)
//FF/Chrome: foo is not defined
//IE: foo is undefined
//Safari: can't find variable foo
仍然不清楚“无法解析的引用(unresolvable reference)”?
在 ECMA 术语中,引用由基值(base value)和引用名(reference name)构成(ECMA 5 8.7 - 我再次忽略了严格模式。还要注意,ECMA 3 的术语略有不同,但实际意义是相同的)。
如果引用是属性,那么基值和引用名位于 .
的两侧(或第一个括号或其他):
window.foo; //base value = window, reference name = foo;
a.b; //base value = a, reference name = b;
myObj['create']; // base value = myObj, reference name = 'create';
//Safari, Chrome, IE8+ only
Object.defineProperty(window,"foo", {value: "hello"}); //base value = window, reference name = foo;
对于变量引用,基值是当前执行上下文的 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 foo;
foo.bar; //TypeError(基值,foo 是未定义的)
bar.baz; //ReferenceError(bar 是不能被解析的)
undefined.foo; //TypeError(基值是未定义的)
变量引用永远会被解析,因为 var 关键字确保 VariableObject 总是被赋给基值。
根据定义,既不是属性也不是变量的引用是不可解析的,并且会抛出一个 ReferenceError:
foo; //ReferenceError
上面的 JavaScript 中没有看到显式的基值,因此会查找 VariableObject 来引用名称为 foo
的属性。确定 foo
没有基值,然后抛出 ReferenceError。
但是 foo
不是一个未声明的变量吗?
技术上不是的。虽然我们有时会发现 “undeclared variable” 是一个错误诊断时有用的术语,但实际上,在变量被声明之前不是变量。
那么隐式全局变量呢?
的确,从未被 var 关键字声明过的标识符将被创建为全局变量 —— 但只有当它们被赋值时才会这样。
function a() {
alert(foo); //ReferenceError
bar = [1,2,3]; //没有错误,foo 是全局的
}
a();
bar; //"1,2,3"
当然,这很烦人。如果 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
来检查引用错误。
我在哪里可以阅读更多?
Mozilla 开发者中心:undefined
Angus Croll:JavaScript 中的变量与属性
Juriy Zaytsev (“kangax”):理解 Delete
Dmitry A. Soshnikov:ECMA-262-3 详解:第 2 章 Variable 对象
undefined:4.3.9, 4.3.10, 8.1
Reference Error:8.7.1, 8.7.2, 10.2.1, 10.2.1.1.4, 10.2.1.2.4, and 11.13.1.
ECMAScript 的严格模式 Annex C
转载自:https://juejin.cn/post/6844903718291243022