第一章:类型 - 值作为类型

优质
小牛编辑
128浏览
2023-12-01

值作为类型

在 JavaScript 中,变量没有类型 — 值才有类型。变量可以在任何时候,持有任何值。

另一种考虑 JS 类型的方式是,JS 没有“类型强制”,也就是引擎不坚持认为一个 变量 总是持有与它开始存在时相同的 初始类型 的值。在一个赋值语句中,一个变量可以持有一个 string,而在下一个赋值语句中持有一个 nubmer,如此类推。

42 有固有的类型 number,而且它的 类型 是不能被改变的。另一个值,比如 string 类型的 "42",可以通过一个称为 强制转换 的处理从 number 类型的值 42 中创建出来(见第四章)。

如果你对一个变量使用 typeof,它不会像表面上看起来那样询问“这个变量的类型是什么?”,因为 JS 变量是没有类型的。取而代之的是,它会询问“在这个变量里的值的类型是什么?”

  1. var a = 42;
  2. typeof a; // "number"
  3. a = true;
  4. typeof a; // "boolean"

typeof 操作符总是返回字符串。所以:

  1. typeof typeof 42; // "string"

第一个 typeof 42 返回 "number",而 typeof "number""string"

undefined vs “undeclared”

当前 还不拥有值的变量,实际上拥有 undefined 值。对这样的变量调用 typeof 将会返回 "undefined"

  1. var a;
  2. typeof a; // "undefined"
  3. var b = 42;
  4. var c;
  5. // 稍后
  6. b = c;
  7. typeof b; // "undefined"
  8. typeof c; // "undefined"

大多数开发者考虑“undefined”这个词的方式会诱使他们认为它是“undeclared(未声明)”的同义词。然而在 JS 中,这两个概念十分不同。

一个“undefined”变量是在可访问的作用域中已经被声明过的,但是在 这个时刻 它里面没有任何值。相比之下,一个“undeclared”变量是在可访问的作用域中还没有被正式声明的。

考虑这段代码:

  1. var a;
  2. a; // undefined
  3. 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 与未声明变量关联的特殊行为,进一步增强了这种困惑。考虑这段代码:

  1. var a;
  2. typeof a; // "undefined"
  3. 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 上的安全防卫就是我们的朋友。

  1. // 噢,这将抛出一个错误!
  2. if (DEBUG) {
  3. console.log( "Debugging is starting" );
  4. }
  5. // 这是一个安全的存在性检查
  6. if (typeof DEBUG !== "undefined") {
  7. console.log( "Debugging is starting" );
  8. }

即便你不是在对付用户定义的变量(比如 DEBUG),这种检查也是很有用的。如果你为一个内建的 API 做特性检查,你也会发现不抛出错误的检查很有帮助:

  1. if (typeof atob === "undefined") {
  2. atob = function() { /*..*/ };
  3. }

注意: 如果你在为一个还不存在的特性定义一个“填补”,你可能想要避免使用 var 来声明 atob。如果你在 if 语句内部声明 var atob,即使这个 if 条件没有通过(因为全局的 atob 已经存在),这个声明也会被提升(参见本系列的 作用域与闭包)到作用域的顶端。在某些浏览器中,对一些特殊类型的内建全局变量(常被称为“宿主对象”),这种重复声明也许会抛出错误。忽略 var 可以防止这种提升声明。

另一种不带有 typeof 的安全防卫特性,而对全局变量进行这些检查的方法是,将所有的全局变量作为全局对象的属性来观察,在浏览器中这个全局对象基本上是 window 对象。所以,上面的检查可以(十分安全地)这样做:

  1. if (window.DEBUG) {
  2. // ..
  3. }
  4. if (!window.atob) {
  5. // ..
  6. }

和引用未声明变量不同的是,在你试着访问一个不存在的对象属性时(即便是在全局的 window 对象上),不会有 ReferenceError 被抛出。

另一方面,一些开发者偏好避免手动使用 window 引用全局变量,特别是当你的代码需要运行在多种 JS 环境中时(例如不仅是在浏览器中,还在服务器端的 node.js 中),全局变量可能不总是称为 window

技术上讲,这种 typeof 上的安全防卫即使在你不使用全局变量时也很有用,虽然这些情况不那么常见,而且一些开发者也许发现这种设计方式不那么理想。想象一个你想要其他人复制-粘贴到他们程序中或模块中的工具函数,在它里面你想要检查包含它的程序是否已经定义了一个特定的变量(以便于你可以使用它):

  1. function doSomethingCool() {
  2. var helper =
  3. (typeof FeatureXYZ !== "undefined") ?
  4. FeatureXYZ :
  5. function() { /*.. 默认的特性 ..*/ };
  6. var val = helper();
  7. // ..
  8. }

doSomethingCool() 对称为 FeatureXYZ 变量进行检查,如果找到,就使用它,如果没找到,使用它自己的。现在,如果某个人在他的模块/程序中引入了这个工具,它会安全地检查我们是否已经定义了 FeatureXYZ

  1. // 一个 IIFE(参见本系列的 *作用域与闭包* 中的“立即被调用的函数表达式”)
  2. (function(){
  3. function FeatureXYZ() { /*.. my XYZ feature ..*/ }
  4. // 引入 `doSomethingCool(..)`
  5. function doSomethingCool() {
  6. var helper =
  7. (typeof FeatureXYZ !== "undefined") ?
  8. FeatureXYZ :
  9. function() { /*.. 默认的特性 ..*/ };
  10. var val = helper();
  11. // ..
  12. }
  13. doSomethingCool();
  14. })();

这里,FeatureXYZ 根本不是一个全局变量,但我们仍然使用 typeof 的安全防卫来使检查变得安全。而且重要的是,我们在这里 没有 可以用于检查的对象(就像我们使用 window.___ 对全局变量做的那样),所以 typeof 十分有帮助。

另一些开发者偏好一种称为“依赖注入”的设计模式,与 doSomethingCool() 隐含地检查 FeatureXYZ 是否在它外部/周围被定义过不同的是,它需要依赖明确地传递进来,就像这样:

  1. function doSomethingCool(FeatureXYZ) {
  2. var helper = FeatureXYZ ||
  3. function() { /*.. 默认的特性 ..*/ };
  4. var val = helper();
  5. // ..
  6. }

在设计这样的功能时有许多选择。这些模式里没有“正确”或“错误” — 每种方式都有各种权衡。但总的来说,typeof 的未声明安全防卫给了我们更多选项,这还是很不错的。