第一章:类型 - 值作为类型
值作为类型
在 JavaScript 中,变量没有类型 — 值才有类型。变量可以在任何时候,持有任何值。
另一种考虑 JS 类型的方式是,JS 没有“类型强制”,也就是引擎不坚持认为一个 变量 总是持有与它开始存在时相同的 初始类型 的值。在一个赋值语句中,一个变量可以持有一个 string
,而在下一个赋值语句中持有一个 nubmer
,如此类推。
值 42
有固有的类型 number
,而且它的 类型 是不能被改变的。另一个值,比如 string
类型的 "42"
,可以通过一个称为 强制转换 的处理从 number
类型的值 42
中创建出来(见第四章)。
如果你对一个变量使用 typeof
,它不会像表面上看起来那样询问“这个变量的类型是什么?”,因为 JS 变量是没有类型的。取而代之的是,它会询问“在这个变量里的值的类型是什么?”
var a = 42;
typeof a; // "number"
a = true;
typeof a; // "boolean"
typeof
操作符总是返回字符串。所以:
typeof typeof 42; // "string"
第一个 typeof 42
返回 "number"
,而 typeof "number"
是 "string"
。
undefined
vs “undeclared”
当前 还不拥有值的变量,实际上拥有 undefined
值。对这样的变量调用 typeof
将会返回 "undefined"
:
var a;
typeof a; // "undefined"
var b = 42;
var c;
// 稍后
b = c;
typeof b; // "undefined"
typeof c; // "undefined"
大多数开发者考虑“undefined”这个词的方式会诱使他们认为它是“undeclared(未声明)”的同义词。然而在 JS 中,这两个概念十分不同。
一个“undefined”变量是在可访问的作用域中已经被声明过的,但是在 这个时刻 它里面没有任何值。相比之下,一个“undeclared”变量是在可访问的作用域中还没有被正式声明的。
考虑这段代码:
var a;
a; // undefined
b; // ReferenceError: b is not defined
一个恼人的困惑是浏览器给这种情形分配的错误消息。正如你所看到的,这个消息是“b is not defined”,这当然很容易而且很合理地使人将它与“b is undefined.”搞混。需要重申的是,“undefined”和“is not defined”是非常不同的东西。要是浏览器能告诉我们类似于“b is not found”或者“b is not declared”之类的东西就好了,那会减少这种困惑!
还有一种 typeof
与未声明变量关联的特殊行为,进一步增强了这种困惑。考虑这段代码:
var a;
typeof a; // "undefined"
typeof b; // "undefined"
typeof
操作符甚至为“undeclared”(或“not defined”)变量返回 "undefined"
。要注意的是,当我们执行 typeof b
时,即使 b
是一个未声明变量,也不会有错误被抛出。这是 typeof
的一种特殊的安全防卫行为。
和上面类似地,要是 typeof
与未声明变量一起使用时返回“undeclared”就好了,而不是将其结果值与不同的“undefined”情况混为一谈。
typeof
Undeclared
不管怎样,当在浏览器中处理 JavaScript 时这种安全防卫是一种有用的特性,因为浏览器中多个脚本文件会将变量加载到共享的全局名称空间。
注意: 许多开发者相信,在全局名称空间中绝不应该有任何变量,而且所有东西应当被包含在模块和私有/隔离的名称空间中。这在理论上很伟大但在实践中几乎是不可能的;但它仍然是一个值得的努力方向!幸运的是,ES6 为模块加入了头等支持,这终于使这一理论变得可行的多了。
作为一个简单的例子,想象在你的程序中有一个“调试模式”,它是通过一个称为 DEBUG
的全局变量(标志)来控制的。在实施类似于在控制台上输出一条日志消息这样的调试任务之前,你想要检查这个变量是否被声明了。一个顶层的全局 var DEBUG = true
声明只包含在一个“debug.js”文件中,这个文件仅在你开发/测试时才被加载到浏览器中,而在生产环境中则不会。
然而,在你其他的程序代码中,你不得不小心你是如何检查这个全局的 DEBUG
变量的,这样你才不会抛出一个 ReferenceError
。这种情况下 typeof
上的安全防卫就是我们的朋友。
// 噢,这将抛出一个错误!
if (DEBUG) {
console.log( "Debugging is starting" );
}
// 这是一个安全的存在性检查
if (typeof DEBUG !== "undefined") {
console.log( "Debugging is starting" );
}
即便你不是在对付用户定义的变量(比如 DEBUG
),这种检查也是很有用的。如果你为一个内建的 API 做特性检查,你也会发现不抛出错误的检查很有帮助:
if (typeof atob === "undefined") {
atob = function() { /*..*/ };
}
注意: 如果你在为一个还不存在的特性定义一个“填补”,你可能想要避免使用 var
来声明 atob
。如果你在 if
语句内部声明 var atob
,即使这个 if
条件没有通过(因为全局的 atob
已经存在),这个声明也会被提升(参见本系列的 作用域与闭包)到作用域的顶端。在某些浏览器中,对一些特殊类型的内建全局变量(常被称为“宿主对象”),这种重复声明也许会抛出错误。忽略 var
可以防止这种提升声明。
另一种不带有 typeof
的安全防卫特性,而对全局变量进行这些检查的方法是,将所有的全局变量作为全局对象的属性来观察,在浏览器中这个全局对象基本上是 window
对象。所以,上面的检查可以(十分安全地)这样做:
if (window.DEBUG) {
// ..
}
if (!window.atob) {
// ..
}
和引用未声明变量不同的是,在你试着访问一个不存在的对象属性时(即便是在全局的 window
对象上),不会有 ReferenceError
被抛出。
另一方面,一些开发者偏好避免手动使用 window
引用全局变量,特别是当你的代码需要运行在多种 JS 环境中时(例如不仅是在浏览器中,还在服务器端的 node.js 中),全局变量可能不总是称为 window
。
技术上讲,这种 typeof
上的安全防卫即使在你不使用全局变量时也很有用,虽然这些情况不那么常见,而且一些开发者也许发现这种设计方式不那么理想。想象一个你想要其他人复制-粘贴到他们程序中或模块中的工具函数,在它里面你想要检查包含它的程序是否已经定义了一个特定的变量(以便于你可以使用它):
function doSomethingCool() {
var helper =
(typeof FeatureXYZ !== "undefined") ?
FeatureXYZ :
function() { /*.. 默认的特性 ..*/ };
var val = helper();
// ..
}
doSomethingCool()
对称为 FeatureXYZ
变量进行检查,如果找到,就使用它,如果没找到,使用它自己的。现在,如果某个人在他的模块/程序中引入了这个工具,它会安全地检查我们是否已经定义了 FeatureXYZ
:
// 一个 IIFE(参见本系列的 *作用域与闭包* 中的“立即被调用的函数表达式”)
(function(){
function FeatureXYZ() { /*.. my XYZ feature ..*/ }
// 引入 `doSomethingCool(..)`
function doSomethingCool() {
var helper =
(typeof FeatureXYZ !== "undefined") ?
FeatureXYZ :
function() { /*.. 默认的特性 ..*/ };
var val = helper();
// ..
}
doSomethingCool();
})();
这里,FeatureXYZ
根本不是一个全局变量,但我们仍然使用 typeof
的安全防卫来使检查变得安全。而且重要的是,我们在这里 没有 可以用于检查的对象(就像我们使用 window.___
对全局变量做的那样),所以 typeof
十分有帮助。
另一些开发者偏好一种称为“依赖注入”的设计模式,与 doSomethingCool()
隐含地检查 FeatureXYZ
是否在它外部/周围被定义过不同的是,它需要依赖明确地传递进来,就像这样:
function doSomethingCool(FeatureXYZ) {
var helper = FeatureXYZ ||
function() { /*.. 默认的特性 ..*/ };
var val = helper();
// ..
}
在设计这样的功能时有许多选择。这些模式里没有“正确”或“错误” — 每种方式都有各种权衡。但总的来说,typeof
的未声明安全防卫给了我们更多选项,这还是很不错的。