第一章、值,类型和操作符

在机器外表以下,程序在运动着。收放自如,毫不费力。电子和谐地散开和重组。显示器上的形状,只不过是水面的涟漪。精髓在不可见的底层。

— Master Yuan-Ma, The Book of Programming

chapter_picture_1

在计算机的世界里,只有数据。你可以读取数据,修改数据和创建新数据。所有的数据以长长的比特序列的形式存储,所以从根本上来说是相似的。

比特是任一种二值的事物,通常描述为 0 和 1。在计算机中,它们可以是或高或低的电荷、或强或弱的信号、CD表面或明或暗的点等形式。任何离散的信息片段都可以简化成 0 和 1 的序列,因此可以用比特表示。

比如,我们可以用比特来表示数字 13。和十进制数字一样,但是只有 2 个不同的数字(而不是 10 个),并且每个数字的权重从右向左以 2 的因子增加。这里是组成数字 13 的比特串(同时在下方显示了每一位数字的权重):

  1. 0 0 0 0 1 1 0 1
  2. 128 64 32 16 8 4 2 1

这就是二进制数 00001101,或 8 + 4 + 1,亦或是 13

想象有许许多多、无穷无尽的比特。一台现代计算机的动态数据存储(工作内存)有超过 300 亿个比特,永久存储(硬盘或同类物)则一般要多出几个数量级。

为了处理如此海量的比特而不丢失,我们需要将它们分割成代表信息片段的块。在JavaScript环境中,这些块叫作“”。尽管所有的值都是由比特组成的,它们扮演着不同的角色。每个值有一个类型来确定它的角色。有些值是数字,有些是文本片段,有些则是函数,等等。

要创建一个值,你只需要调用它的名称,这就很方便了。你不需要收集这些值的创建材料,也不需要花钱。你只需要发出请求,然后“嗖地一声”,你所需要的值就有了。当然了,它们也不是凭空就能创建出来。每个值都必须保存在某个地方,如果你想同时使用巨量的值,则有可能耗光整个内存。好在这个问题只在你同时需要所有这些值时才存在。一旦你不再使用一个值,它就会释放出它的比特供回收,以用作下一次产生值时的原材料。

本章介绍JavaScript程序的基本单元,即:简单值类型,以及可在这些值上进行的操作符。

数值

毋庸置疑,数值 类型的值指的是数字值。在JavaScript程序中,它们被写作:

  1. 13

在程序中使用时,它会以数字 13 的比特形式载入计算机的内存中。

JavaScript使用固定数量的比特(64位)来存储一个数字值。使用 64 位比特一共只有一定数量的组合模式,也就是说,可以表示的数字数量是有限的。对于 N 位十进制数字,可以表示的数字数量为 10N。类似地,对于 64 位二进制数,可以表示 264 个不同的数,即 18 百京(18 再跟 18 个 0)。这已经很多了。

计算机内存一般要小得多,人们通常用 8 或 16 位的比特组来表示数字。很容易不小心溢出这么小的数字:产生的数字无法以给定数量的比特来表示。现如今,即使是便携式电脑也有大容量的内插,你可以轻松地使用 64 位的块,而且只有当你真正要处理天文数字时才需要考虑溢出的问题。

但是,并不是所有 18 百京以内的整数都能表示为JavaScript中的数字。还需要保存负数,所以其中 1 个比特表示的是数字的符号。更严重的问题是,非整数也需要表示出来。为了处理这个问题,一些比特被用来保存小数点的位置。可喜的是,实际可保存的最大整数大概是在 9 万亿(15 个 0),仍然非常大。

小数使用“点”来表示:

  1. 9.81

对于极大或极小的数字,也可以用科学计数法:添加 1 个e(表示指数),后面再跟上那个数的指数:

  1. 2.998e8

也就是 2.998 × 108 = 299,800,000。

对于上述所说的 9 万亿以内整数(也叫作:整型数)的计算,总是可以保证是精确的。不幸的是,对于小数的计算就通常不是了。正如 π (pi,圆周率)无法用有限个十进制数字精确表示出来,很多数字在只有 64 个比特可用来保存时会丢失一些精确度。这比较遗憾,不过它只在一些特定的情况下会引发实际的问题。重要的是,要注意到这个问题,并把小数看作是一种近似值,而不是精确值。

算术

数字主要用来做算术。算术运算,比如加或乘,需要 2 个值,并通过它们生成一个新的数字。在JavaScript中,类似于这样:

  1. 100 + 4 * 11

符号 +* 叫作操作符。第一个代表加法,第二个代表乘法。把操作符放在 2 个值中间,会将其作用于那些值,并产生一个新值。

但是,这个例子表示的是“4 与 100 相加,再将结果与 11 相乘”,还是乘法在加法之前运行呢?正如你可能猜的那样,乘法先进行运算。不过,和数学中一样,可以通过把加法放入括号中来改变运算顺序:

  1. (100 + 4) * 11

对于减法,可以用 - 操作符,除法则用 / 操作符来完成。

当有多个操作符而没有使用括号时,运算的顺序由运算符的 优先级 决定。上面的例子说明乘法在加法之前进行。/ 操作符和 * 的优先级相同。+- 也是一样的。当多个相同优先级相邻出现时,比如 1 - 2 + 1,则从左到右执行:(1 - 2) + 1

不用过于担心这些优先级的规则。当不确定时,只需要加上括号。

还有一个算术操作符,你可能还不认识。% 符号表示 求余 运算。X % Y 表示 XY 除所得的余数。例如,314 % 100 结果是 14,而 144 % 12 则为 0。求余的优先级与乘除法相同。很多地方也称这个操作符为 求模

特殊数字

JavaScript中有 3 个特殊值被看作数字,但与普通数字表现不同。

前两个是 Infinity-Infinity,分别表示正无穷和负无穷。Infinity - 1 仍然是 Infinity,以此类推。然而,不要太过相信基于无穷的计算。它在数学上不那么可靠,而且很快就引出下一个特殊数字:NaN

NaN表示“不是数字”,然而它确实 一个数字类型的值。当你尝试计算 0 / 0(0 被 0 相除),Infinity - Infinity,或任意无法生成有意义结果的数学运算时,将得到这个结果。

字符串

下一个基本数据类型是 字符串。字符串用来表示文本。书写时把内容放入引号中:

  1. `Down on the sea`
  2. "Lie on the ocean"
  3. 'Float on the ocean'

可以使用单引号、双引号或者反引号来标记字符串,只要引号的开头和结尾匹配即可。

几乎所有内容都可以放进引号中,JavaScript会相应生产一个字符串值。但对于个别字符来说,就更加困难了。你可以想象一下引号中再放入引号会有多难。换行符 (当敲回车时得到的字符)只有在用反引号(`)引用时可以不用转义。

为了在字符串中包含这样的字符,需要用到如下标记法:在引用的文本中出现反斜杠(\)时,这表示紧跟着的那个字符具有特殊含义。这叫做 转义 字符。含有反斜杠的引用,不会终止字符串,而是它的一部分。当一个 n 字符出现在反斜杠之后时,表示一个换行符。同样地,反斜杠和 t 组合则表示制表符。考虑如下字符串:

  1. "This is the first line\nAnd this is the second"

其包含的真实文本将是:

  1. This is the first line
  2. And this is the second

当然,也有很多情况:你不想让反斜杠成为特殊字符,就当作一个反斜杠。如果 2 个反斜杠紧挨着,它们会“折叠”在一起:只有一个会留下来出现在最终的字符串值中。比如,字符串 “A newline character is written like "\n".” 可以如下表示:

  1. "A newline character is written like \"\\n\"."

字符串也必须建模成比特串,以供计算机使用。JavaScript基于 Unicode 标准来做这件事。这个标准会指定一个数字给几乎每一个你需要的字符,包括希腊语、阿拉伯语、日语、亚美尼亚语等等的字符。如果对于每一个字符都有一个数字来对应,那么字符串就可以通过一系列的数字来描述了。

JavaScript就是这么干的。但是有一个难点:JavaScript使用共可表示 216 个不同的数字的 16 位比特来表示一个字符串元素。而Unicode定义了更多的字符,差不多 2 倍。因此,有些字符,比如 emoji,在JavaScript字符串中占 2 个“字符位置”。我们将在第五章重新回到这个话题。

字符串不能相除、相乘或相减,但是 + 操作符却可以用于它们。它做的不是加法,而是 拼接:把两个字符串“粘”在一起。下面一行将生产字符串 "concatenate"

  1. "con" + "cat" + "e" + "nate"

字符串有很多相关的函数(方法)用来对其进行其他操作。我们将在第四章继续讨论。

字符串使用单引号或双引号,效果基本一样,唯一的不同在于:在哪一种引号中需要转义。反引号括起来的字符串,通常称为“模板字符串”,有一些额外的技巧。除了能跨行,它们还能嵌入其他值。

  1. `half of 100 is ${100 / 2}`

当在模板字符串中,当你在 ${} 内写入了一些内容时,那么它的结果会被计算并转为字符串,然后插入到相应位置上。上述例子结果为 “half of 100 is 50”。

一元操作符

并非所有操作符都是符号,有一些被写成关键字的形式。一个例子就是 typeof 操作符,它会生成一个字符串值,表明你所传给它的值的类型。

  1. console.log(typeof 4.5)
  2. // → number
  3. console.log(typeof "x")
  4. // → string

我们在例子中用了 console.log ,表明我们想查看对某些内容求值之后的结果。更多内容,请查看下一章

我们所看到的其他操作符,都是作用在两个值的,而 typeof 只需要一个。需要用到两个值的操作符叫做 二元 操作符,而那些只需要一个的则叫做 一元 操作符。-(减号)操作符既可以用作二元操作符,也可以用作一元操作符。

  1. console.log(- (10 - 2))
  2. // → -8

布尔值

很多时候,有一个能区分仅有的两种可能性(比如“yes”和“no”,或“on”和“off”)的值,会非常有用。为此,JavaScript中有一个 布尔 类型,它只有 2 个值:true(真) 和 false(假)。

比较

这里是生成布尔值的一种方式:

  1. console.log(3 > 2)
  2. // → true
  3. console.log(3 < 2)
  4. // → false

>< 就是传统的符号,分别表示“大于”和“小于”。它们是二元操作符。使用它们时,会生成一个布尔值,表明了在当前情况下是否为真。

字符串也可以同样的方式进行比较。

  1. console.log("Aardvark" < "Zoroaster")
  2. // → true

字符串的排序基本上是按照字母表顺序的,但又不完全是你期望的在字典里看到的那样:大写字母总是“小于”小写字母,因此 "Z" < "a",并且非字母表字符(!-等等)也以此方式排序。当比较字符串时,JavaScript从左至右查看字符,一个一个地比较对应的 Unicode 编码。

其他类似的操作符还有 >= (大于或等于),<= (小于或等于),== (等于) 和 != (不等于)。

  1. console.log("Itchy" != "Scratchy")
  2. // → true
  3. console.log("Apple" == "Orange")
  4. // → false

JavaScript中只有一个值不等于它本身,这就是 NaN(“不是数字”)。

  1. console.log(NaN == NaN)
  2. // → false

NaN 被设计用来表示无意义计算的结果,因此,它不等于任何 其他 无意义计算的结果。

逻辑操作符

也有一些操作可以用于布尔值本身。JavaScript支持 3 种逻辑操作符:。它们可以被用来对布尔值进行“推理”。

&& 操作符代表逻辑 。它是一个二元操作符,当且仅当传给它的值都为真时,它的结果才为真。

  1. console.log(true && false)
  2. // → false
  3. console.log(true && true)
  4. // → true

|| 操作符表示逻辑 。传给它的任一个值为真,它就会返回真。

  1. console.log(false || true)
  2. // → true
  3. console.log(false || false)
  4. // → false

被写成一个叹号(!)。它是一元操作符,会将传给它的值翻转:!true 生成 false,而 !false 则生成 true

当把这些逻辑操作符与算术及其他操作符混用时,何时需要用到括号往往不那么明显。实际上,你对目前所见到的操作符通常差不多了解了,|| 优先级最低,然后是 &&,接着是比较操作符(>==等),而后是其他操作符。这个次序确定后,类似于下面这个例子的表达式,就可以尽可能少地使用括号:

  1. 1 + 1 == 2 && 10 * 10 > 50

我要讨论的最后一个逻辑操作符,既不是一元的也不是二元的,而是 三元的,作用于 3 个值。它有一个问号和一个冒号,像这样:

  1. console.log(true ? 1 : 2);
  2. // → 1
  3. console.log(false ? 1 : 2);
  4. // → 2

这个操作符叫作 条件操作符(有时候也叫作 三元操作符,因为这是该语言中唯一的此类操作符)。问号左侧的值将“挑选”最终结果是来自于另外两个值中的哪一个。当它为真时,则选中间那个值;为假时,则选右边那个值。

空值

有 2 个特殊值,nullundefined,用来表示“没有意义的值”。它们是值,但却不携带信息。

语言中很多不能生成一个有意义的值的操作(一会儿会看到例子)就会生成 undefined,因为它们必须生成 某些 值。

nullundefined 意义上的不同,是JavaScript设计的一个意外,大多数时候不太重要。在真正必须考虑这些值的情况中,我建议把它们基本上当作可互相替换的值。

自动类型转换

我曾经在简介中提到,JavaScript可以接受你给它的几乎任何程序,甚至是做奇怪事情的程序。下面的表达式可以充分说明这一点:

  1. console.log(8 * null)
  2. // → 0
  3. console.log("5" - 1)
  4. // → 4
  5. console.log("5" + 1)
  6. // → 51
  7. console.log("five" * 2)
  8. // → NaN
  9. console.log(false == 0)
  10. // → true

当一个操作符被用于“错误”类型的值时,JavaScript会静默地把那个值转换成它需要的类型(使用的规则通常不像你想象中那样)。这叫做 强制类型转换。第一个表达式中的 null 变成了 0,第二个表达式中的 "5" 变成了 5(字符串转换成数字)。而在第三个表达式中,+ 会优先于数字加法来执行字符串拼接,因此 1 被转换成 "1"(数字变成字符串)。

当某些不能以明确的方式映射成数字的值(比如:"five"undefined)被转换成数字时,将会得到 NaN 值。对 NaN 的后续算术运算将一直生成 NaN,所以,如果你发现在一个意外的地方得到了这样的值,就需要查找意外的类型转换。

当用 == 来比较同类型的值时,结果很容易预测:当两个值相同时为真,有 NaN 的情况除外。当类型不同时,JavaScript有一套复杂难懂的规则来确定要做什么。大多数情况下,它会尝试把其中一个值转换为另一个值的类型。然而,当 nullundefined 出现在操作符的任一边时,当且仅当两边的值同为 nullundefined 其中之一时,结果为真。

  1. console.log(null == undefined);
  2. // → true
  3. console.log(null == 0);
  4. // → false

这个行为通常很有用。当你想测试一个值是否真实有值而不是 nullundefined 时,就可以把它与 null==(或 !=)操作符来比较。

但是,当你想测试某个值是否指代值 false 时,该怎么办呢?把字符串和数字转换为布尔值的规则表明:0NaN 和空字符串("")算作 false,而所有其他值都算作 true。由此得知,表达式如 0 == false"" == false 均为真。如果你 想发生任何自动类型转换,有另外 2 个操作符:===!==。第一个检查一个值是否 精确 等于另一个值,第二个则检查它是否精确地不等。因此,"" === false 为假,与预期一致。

我建议使用三字符的比较操作符来避免意外的类型转换而导致错误。不过,如果你能确定两边的类型相同,使用短操作符就完全没有问题。

逻辑操作符的“短路”

逻辑操作符 &&|| 用特有的方式来处理不同类型的值。它们会把左边的值转换为布尔值,以确定接下来做什么;但是,根据操作符的不同以及转换的结果,它们将要么返回 原始 左边的值或者右边的值。

比如,|| 操作符在左值可被转化为真时返回左值,否则返回右值。当值是布尔型时,有着预期的效果,对于其他类型的值也是类似的。

  1. console.log(null || "user")
  2. // → user
  3. console.log("Agnes" || "user")
  4. // → Agnes

我们可以把这个特性作为设置默认值的一种方式。如果你有一个可能为空的值,就可以在其后面加上 || 和一个替代值。如果原始值可转化为假,将得到那个替代值。

&& 操作符工作方式类似,只不过刚好相反。当左值可转化为假时返回左值,否则返回右值。

这两个操作符的另一个重要特性是:右边的部分只在需要的时候会被求值。对 true || X 来说,无论 X 是什么 —— 即使它是一段 糟糕 的程序,结果都为 true,而 X 永远不会被求值。同样地,对于 false && X,结果为 false 并忽略 X。这叫作 短路求值

条件操作符也是类似的工作方式。对于第二个和第三个值,只有被选中的那个才会被求值。

总结

本章我们看了JavaScript值的 4 种类型:数字,字符串,布尔值和未定义的值。

通过键入名称(truenull)或值(13"abc")来创建值。可以用操作符来组合及转换这些值。我们看到了二元操作符来进行算术运算(+-*/%)、字符串拼接(+)、比较(==!====!==<><=>=)和逻辑运算(&&||),还有几个一元操作符(- 对数字取反,! 进行逻辑取反,typeof 查找一个值的类型)和一个三元操作符(?:)来基于第三个值来挑选两个值中的一个。

这些已经足够让你把JavaScript当成一个便携计算器来使用了,但也仅此而已。下一章将把这些表达式组合在一起组成基本的程序。