1.3 值和类型
JavaScript 中的变量是没有类型的,只有值才有 。变量可以随时持有任何类型的值。
换个角度来理解就是,JavaScript 不做“类型强制”;也就是说,语言引擎不要求变量 总是持有与其初始值同类型 的值。一个变量可以现在被赋值为字符串类型值,随后又被赋值为数字类型值。
42 的类型为 number ,并且无法更改。而 "42" 的类型为 string 。数字 42 可以通过强制类型转换 (coercion)为字符串 "42" (参见第 4 章)。
在对变量执行 typeof 操作时,得到的结果并不是该变量的类型,而是该变量持有的值的类型,因为 JavaScript 中的变量没有类型。
var a = 42; typeof a; // "number" a = true; typeof a; // "boolean"
typeof 运算符总是会返回一个字符串:
typeof typeof 42; // "string"
typeof 42 首先返回字符串 "number" ,然后 typeof "number" 返回 "string" 。
1.3.1 undefined 和 undeclared
变量在未持有值的时候为 undefined 。此时 typeof 返回 "undefined" :
var a; typeof a; // "undefined" var b = 42; var c; // later b = c; typeof b; // "undefined" typeof c; // "undefined"
大多数开发者倾向于将 undefined 等同于 undeclared(未声明),但在 JavaScript 中它们完全是两回事。
已在作用域中声明但还没有赋值的变量,是 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 处理 undeclared 变量的方式。例如:
var a; typeof a; // "undefined" typeof b; // "undefined"
对于 undeclared(或者 not defined)变量,typeof 照样返回 "undefined" 。请注意虽然 b 是一个 undeclared 变量,但 typeof b 并没有报错。这是因为 typeof 有一个特殊的安全防范机制。
此时 typeof 如果能返回 undeclared(而非 undefined)的话,情况会好很多。
1.3.2 typeof Undeclared
该安全防范机制对在浏览器中运行的 JavaScript 代码来说还是很有帮助的,因为多个脚本文件会在共享的全局命名空间中加载变量。
很多开发人员认为全局命名空间中不应该有变量存在,所有东西都应该被封装到模块和私有 / 独立的命名空间中。理论上这样没错,却不切实际。然而这仍不失为一个值得为之努力奋斗的目标。好在 ES6 中加入了对模块的支持,这使我们又向目标迈近了一步。
举个简单的例子,在程序中使用全局变量 DEBUG 作为“调试模式”的开关。在输出调试信息到控制台之前,我们会检查 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() { /*..*/ }; }
如果要为某个缺失的功能写 polyfill(即衬垫代码或者补充代码,用来补充当前运行环境中缺失的功能),一般不会用 var atob 来声明变量 atob 。如果在 if 语句中使用 var atob ,声明会被提升(hoisted,参见《你不知道的 JavaScript(上卷)》1 中的“作用域和闭包”部分)到作用域(即当前脚本或函数的作用域)的最顶层,即使 if 条件不成立也是如此(因为 atob 全局变量已经存在)。在有些浏览器中,对于一些特殊的内建全局变量(通常称为“宿主对象”,host object),这样的重复声明会报错。去掉 var 则可以防止声明被提升。
1 此书已由人民邮电出版社出版。——编者注
还有一种不用通过 typeof 的安全防范机制的方法,就是检查所有全局变量是否是全局对象的属性,浏览器中的全局对象是 window 。所以前面的例子也可以这样来实现:
if (window.DEBUG) { // .. } if (!window.atob) { // .. }
与 undeclared 变量不同,访问不存在的对象属性(甚至是在全局对象 window 上)不会产生 ReferenceError 错误。
一些开发人员不喜欢通过 window 来访问全局对象,尤其当代码需要运行在多种 JavaScript 环境中时(不仅仅是浏览器,还有服务器端,如 node.js 等),因为此时全局对象并非总是 window 。
从技术角度来说,typeof 的安全防范机制对于非全局变量也很管用,虽然这种情况并不多见,也有一些开发人员不大愿意这样做。如果想让别人在他们的程序或模块中复制粘贴你的代码,就需要检查你用到的变量是否已经在宿主程序中定义过:
function doSomethingCool() { var helper = (typeof FeatureXYZ !== "undefined") ? FeatureXYZ : function() { /*.. default feature ..*/ }; var val = helper(); // .. }
其他模块和程序引入 doSomethingCool() 时,doSomethingCool() 会检查 FeatureXYZ 变量是否已经在宿主程序中定义过;如果是,就用现成的,否则就自己定义:
// 一个立即执行函数表达式(IIFE,参见《你不知道的JavaScript(上卷)》 “作用域和闭包” // 部分的3.3.2节) (function(){ function FeatureXYZ() { /*.. my XYZ feature ..*/ } // 包含doSomethingCool(..) function doSomethingCool() { var helper = (typeof FeatureXYZ !== "undefined") ? FeatureXYZ : function() { /*.. default feature ..*/ }; var val = helper(); // .. } doSomethingCool(); })();
这里,FeatureXYZ 并不是一个全局变量,但我们还是可以使用 typeof 的安全防范机制来做检查,因为这里没有全局对象可用(像前面提到的 window.___ )。
还有一些人喜欢使用“依赖注入”(dependency injection)设计模式,就是将依赖通过参数显式地传递到函数中,如:
function doSomethingCool(FeatureXYZ) { var helper = FeatureXYZ || function() { /*.. default feature ..*/ }; var val = helper(); // .. }
上述种种选择和方法各有利弊。好在 typeof 的安全防范机制为我们提供了更多选择。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论