【译】理解 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?

是的,一般都很好理解,但是还需要重申的是:undefinednull 不同,null 表示有意的缺少值的原始值。undefinednull 唯一的相似之处是,它们都为 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 对象

ECMA-262 第五版标准文档

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

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

猜你喜欢

转载自juejin.im/post/5bf57e8ef265da612c5d8439