译者的前言
一直都想好好研究这个在 GitHub 上很有名气的系列,而翻译恰是最好的阅读途径之一。可以让我阅读的时候,不那么不求甚解。
图灵社区出版了该系列两部分的中文版——《作用域和闭包》以及《this和对象原型》,我就打算从《类型和语法》这本开始做起。
同时,我对本书的翻译进度会在 GitHub 上同步,希望能有更多的同行参与进来,将更多的干货贡献社区。
翻译 GitHub 仓库地址:https://github.com/StarStudio/You-Dont-Know-JS
PS:最近对于翻译英文原版系列很有兴趣,如果有好的干货英文文章(且大家信得过我的水平),可以放在评论区,有时间我一定会翻译!
第一章:类型
大多数开发人员认为,动态语言(如 JavaScript)并没有类型。让我们来看看 ES5.1 的 规范 对于这部分内容是怎么说的:
本规范中所有算法所操作的值都有一个类型与之对应。这些值的类型均在本规范中对应。当然,这些类型也可能是 ECMAScript 语言中规定的类型的子类型。
在 ECMAScript 语言中,每个 ECMAScript 类型所对应的值都被 ECMAScript 程序开发人员直接操作。ECMAScript 语言中规定的类型为 Undefined, Null, Boolean, String, Number 以及Object。
如果你是强类型语言(静态语言)的粉丝,你也许会对这样使用“类型”感到很反感。在那些语言里,“类型”所拥有的含义可比在 JS 里的多得多。
有人说 JS 不应该声称它有“类型,应该把这种东西称为“标签”,或是“子类型”。
好吧。我们将使用这一粗略的定义(类似于规范中所描述的):一个类型是一个固有的,内建的特征集,无论是编译引擎还是开发人员,都可以用它来确定一个值的行为,并把这个值和其他值加以区分。
简单来说,如果在编译引擎和开发人员眼里,值 42
(数字)和值 "42"
(字符串)处理的方法不同,那么我们就说他们有不同的类型—— number
和 string
。当你处理 42
时,你将使用一些处理数字的方法,比如数学运算。而当你处理 "42"
时,你则会使用一些字符串处理方法,比如输出到页面,等等。这两个值有不同的类型。
虽然这并不是什么严谨的定义,但对于我们接下来的讨论,已经绰绰有余了。而且这样的定义,和JS如何形容自己是一致的。
类型——或是别的什么
不考虑学术上的争论,我们来想想为什么 JavaScript 会需要类型?
对于每种类型及其基本行为都有所了解,有助于更高效的将值进行类型转换(详见第四章,类型转换)。几乎所有的JS程序,都存在着这样那样的类型转换,所以了解这些,对你来说很重要。
如果你有一个值为 42
的 number
,但想对它进行 string
类型的操作,如移除 1
位置的字符 "2"
,你最好先将这个值的类型从 number
转换为 string
。
这看似很简单。
但是进行这样的类型转换,有很多方式。有些方式很明确,很简单就能说出来龙去脉,并且也值得信赖.但如果你不够细心,类型转换可能以一种匪夷所思的方式展现在你面前。
类型转换可能是 JavaScript 最大的疑惑之一了。这点经常被视为这一语言的缺陷,是应该避免使用的。
由于有了对 JavaScript 类型的全面了解,我们希望能够说明为何类型转换的坏名声言过其实,甚至是不恰当的——我们会改变你的传统观点,让你看到类型转换的强大力量和实用性。不过首先,我们先来了解一下值和类型。
内建类型
JavaScript 定义了七种内建类型:
null
undefined
boolean
number
string
object
symbol
—— ES6 中新增
提示:以上类型,除 object
的被称为基本类型。
typeof
运算符会检测所给值得类型,并返回以下其中字符串类型的值——然而奇怪的是,返回的结果和我们刚刚列出的的内建类型并不一一对应。
typeof undefined === "undefined"; // true
typeof true === "boolean"; // true
typeof 42 === "number"; // true
typeof "42" === "string"; // true
typeof { life: 42 } === "object"; // true
// ES6新增!
typeof Symbol() === "symbol"; // true
列出的六种类型的值都会返回一个对应类型名称的字符串。Symbol
是 ES6 中新增的数据类型,我们会在第三章详细介绍。
你也许注意到了,我将 null
从列表中除去了。因为他很特殊——当使用 typeof
运算符时,它表现的就像 bug 一样:
typeof null === "object"; // true
如果它返回的是 "null"
的话,那可真是件好事,可惜的是,这个 bug 已经存在了 20 年,而且由于有太多的 web 程序依赖这一 bug 运行,修复这一 bug 的话,将会创造更多的 bug,并且使很多 web 应用无法运行,所以估计将来也不会修复。
如果你想要确定一个 null
类型的值是这一类型,你需要使用复合判定:
var a = null;
(!a && typeof a === "object"); // true
null
是基本类型中唯一值表现的像 false 一样的类型(详见第四章),但如果运行 typeof
进行检查,返回的还是 "object"
。
那么,typeof
返回的第七种字符串类型的值是什么?
typeof function a(){ /* .. */ } === "function"; // true
单拍脑袋想的话,很容易理解 function
(函数)会是 JS 中顶级的内建类型,尤其是它针对 typeof
运算符的表现。然而,如果你阅读相关的标准,会发现它实际上是对象类型(object
)的子类型。更确切的说,函数是一种“可以被调用的对象”——一类拥有名为 [[Call]]
的内建属性且可以被调用的对象。
函数实际上是对象这点其实很有用。最重要的一点就是,它可以有属性。例如:
function a(b,c) {
/* .. */
}
该函数具有一个 length
属性,值为函数形式参数的个数。
a.length; // 2
本例中,函数声明中包括两个形参(b
和 c
),所以“函数的长度”是 2
。
那么数组呢?他们也是 JS 内置的类型,会不会有什么特殊的表现?
typeof [1,2,3] === "object"; // true
然而并没有,只是普通的对象罢了。一般将它们也视为对象的“子类型”(详见第三章),与普通对象不同的是,它们可以通过数字来序列化(就像普通对象那样可以通过字符串类型的 key(键)来序列化一样),并且操作有可以自动更新的 length
属性。
值和类型
在 JavaScript 中,变量不具有类型——值有类型。变量可以在任何时刻保存任何值。
换句话说,JS 并不是强类型的语言,编译引擎不会让一个变量始终保存和这个变量最开始所保存的值拥有相同的类型。变量可以保存一个 string
类型的值,并在接下来的赋值操作中保存一个number
类型,以此类推。
一个值
如 42
是 number
类型的,而且这个类型是不能改变的。另一个值,如 "42"
是 string
类型,可以通过对 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)”相当于是“未声明”的代名词,然而在 JS 中,这两个概念截然不同。
一个“未定义(undefined)”的变量是已经在当前作用域中声明了的,只不过是目前它并没有保存其他的值而已。而“未声明(undeclared)”则是指在当前作用域中没有声明的变量。
考虑如下的示例:
var a;
a; // undefined
b; // ReferenceError: b is not defined(错误的中文大意是:引用错误:b 尚未定义)
浏览器对于这一错误的描述可以说相当让人困惑。“b 尚未定义”很容易让人理解成“b 是未定义”。然后,“未定义”和“尚未定义”间的差别实在是太大了。如果浏览器要是能报个像“未找到变量 b”或是“b 尚未声明”之类的错误,就不会这么让人迷糊了。
同样的,typeof
运算符的特殊行为加重了这一困惑,请看例子:
var a;
typeof a; // "undefined"
typeof b; // "undefined"
对于“未声明”或着说“尚未定义”的变量,typeof
会返回 "undefined"
。你会发现,虽然 b
是一个没有声明的变量,但是当我们执行 typeof b
的时候却没有报错。会出现这种情况,源于 typeof
运算符特殊的安全机制。
和前面的例子一样,如果对于没有声明的变量,typeof
会返回一个“未声明”之类的东西,而不是将其和“undefined”混为一谈的话,就不会有这么多麻烦了。
typeof
对处理未声明的处理
然而,在浏览器端这种,多个脚本文件均可以在全局命名空间下加载变量的 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" );
}
这类检查不仅对于用户定义的变量很有用,当你在见此一个内建的 API 的时候,这种不会抛出错误的检查也非常棒:
if (typeof atob === "undefined") {
atob = function() { /*..*/ };
}
提示:当你在对一个目前不存在的特性写“polyfill(腻子脚本)”的时候,你需要避免用 var
来声明变量 atob
。如果你在 if
语句里面使用 var atob
来声明,即使 if 语句的条件不满足,变量的声明也会被提升到作用域的最顶级(详见本系列中的《作用域和闭包》)。在部分浏览器中,对一些特殊的全局的内建对象类型(常称为“宿主对象”,如浏览器中的 DOM 对象),这种重复的声明会报错。所以最好避免使用 var
来阻止变量提升。
另一种不使用 typeof
安全机制,进行检查的方法,就是利用所有的全局变量都是(global)全局对象(在浏览器中就是 window
对象)这一点。所以,上面的检查还有如下等价的写法(同样很安全):
if (window.DEBUG) {
// ..
}
if (!window.atob) {
// ..
}
和引用一个未声明的变量不同,当你尝试获取一个对象(即便是 window
对象)不存在的属性的时候,并不会抛出什么 ReferenceError
。
而另一方面,一些开发者极力避免使用 window
对象来引用全局变量,尤其是当你的代码运行在多种 JS 环境(不光是浏览器,比如服务端的 node.js)时,全局(global)对象可不一定叫 window
。
即便当你不使用全局变量的时候,typeof
的安全机制也有它的用武之地,虽然这种情况很少见,也有一些开发人员认为这种设计并不值得。比如你准备写一个可供他人复制粘贴的通用函数,想要知道程序中是否定义了某一特定的变量(将会影响你函数的执行),你可以这样:
function doSomethingCool() {
var helper =
(typeof FeatureXYZ !== "undefined") ?
FeatureXYZ :
function() { /*.. 默认值 ..*/ };
var val = helper();
// ..
}
doSomethingCool()
会检查是否存在一个名为 FeatureXYZ
的变量,有的话就使用,没有的话,就使用默认值。现在,如果有人在他的程序/模块中使用了这一公共函数,检查它们是否定义了 FeatureXYZ
就显得尤为重要:
// IIFE (详见本系列《作用域和闭包》一书中的立即执行函数表达式)
(function(){
function FeatureXYZ() { /*.. my XYZ feature ..*/ }
// include `doSomethingCool(..)`
function doSomethingCool() {
var helper =
(typeof FeatureXYZ !== "undefined") ?
FeatureXYZ :
function() { /*.. default feature ..*/ };
var val = helper();
// ..
}
doSomethingCool();
})();
在这里,FeatureXYZ
并不是一个全局变量,但我们仍然使用 typeof
运算符的安全机制来检查。注意到,在这种情况下,我们可没有全局对象用于这一检查(像使用 window.___
那样),所以 typeof
真的很有帮助。
有些开发者可能会喜欢一种叫做“依赖注入”的设计模式,让 doSomethingCool()
不去检查 FeatureXYZ
是否在它外部/附近被定义,而是通过显示的判断来确定,如:
function doSomethingCool(FeatureXYZ) {
var helper = FeatureXYZ ||
function() { /*.. 默认值 ..*/ };
var val = helper();
// ..
}
要实现这一功能,其实有很多解决方案。没有一种模式是“对的”或“错的”——要对各种方法进行权衡。不过总的来说,typeof
的安全机制确实给了我们更多的选择。
总结
JavaScript 拥有七种内建类型:null
,undefined
,boolean
,number
,string
,object
,symbol
。可以通过使用 typeof
运算符来对它们进行区分。
变量不具有类型,但值有。这些类型定义了值的行为。
许多开发者会将“未定义(undefined)”和“未声明”混为一谈,但是在 JavaScript 它们完全不同。undefined
是一个可供已经声明的变量保存的值。“未声明”意味着一个未经声明的变量。
不幸的是,JavaScript 中很多地方都将两者混为一谈,比如错误信息("ReferenceError: a is not defined"),以及用 typeof
操作,两者都返回 "undefined"
。
不过,typeof
这种安全机制(阻止报错)在某些场景中,如需要检查一个变量是否存在的时候还是很有用的。
原书 《You Don't Know JS: Types & Grammar》
本章原文 Chapter 1: Types
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。