第 3 版已发布。点击这里阅读

第 1 章
值、类型和运算符

在机器表面的下方,程序在移动。它毫不费力地扩展和收缩。电子在完美的和谐中散布和重组。显示器上的形式不过是水面上的涟漪。本质隐形地潜藏在下方。

元马大师,编程之书

在计算机的世界里,只有数据。你可以读取数据,修改数据,创建新数据——但任何不是数据的东西都不存在。所有这些数据都存储为长长的位序列,因此从根本上来说是相同的。

位是任何类型的二值事物,通常被描述为零和一。在计算机内部,它们以高电荷或低电荷、强信号或弱信号,或 CD 表面上的光亮斑点或暗淡斑点等形式存在。任何离散的信息都可以简化为零和一的序列,从而用位表示。

例如,考虑一下如何用位表示数字 13。它与写十进制数的方式相同,但不是 10 个不同的数字,你只有 2 个,每个数字的权重从右到左增加 2 倍。以下是构成数字 13 的位,下面显示了每个数字的权重

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

所以这就是二进制数 00001101,也就是 8 + 4 + 1,等于 13。

想象一片位的海洋。一个位的海洋。一台典型的现代计算机在其易失性数据存储中拥有超过 300 亿位。非易失性存储(硬盘或类似设备)往往还有几个数量级更多。

The Ocean of Bits

为了能够处理如此大量的位而不会迷失,你可以将它们分成表示信息片段的块。在 JavaScript 环境中,这些块被称为。尽管所有值都是由位组成的,但它们扮演着不同的角色。每个值都有一个类型,该类型决定了它的角色。JavaScript 中有六种基本类型的值:数字、字符串、布尔值、对象、函数和未定义值。

要创建一个值,你只需要调用它的名称。这很方便。你不需要为你的值收集建筑材料或支付费用。你只需要呼唤一个,然后,你就拥有了它。当然,它们不是凭空产生的。每个值都必须存储在某个地方,如果你想同时使用大量的值,你可能会用完位。幸运的是,这只有在你需要同时使用它们时才会成为问题。一旦你不再使用一个值,它就会消失,留下它的位,以便作为下一代值的建筑材料进行回收。

本章介绍了 JavaScript 程序的原子元素,即简单的值类型以及可以对这些值进行操作的运算符。

数字

数字类型的值,毫不奇怪,是数值。在 JavaScript 程序中,它们按如下方式写出

13

在程序中使用它,它将导致数字 13 的位模式在计算机内存中出现。

JavaScript 使用固定数量的位,即 64 位,来存储单个数字值。用 64 位只能做出有限数量的模式,这意味着可以表示的不同数字的数量是有限的。对于N 个十进制数字,可以表示的数字数量为 10N。同样,给定 64 个二进制数字,你可以表示 264 个不同的数字,大约为 18 个五千万亿(后面有 18 个零的 18)。这很多。

计算机内存过去要小得多,人们倾向于使用 8 位或 16 位的组来表示他们的数字。很容易意外地溢出这些较小的数字——最终得到一个不适合给定位数的数字。如今,即使是个人电脑也有充足的内存,因此你可以随意使用 64 位块,这意味着你只需要在处理真正天文数字时才需要担心溢出。

不过,并非所有小于 18 个五千万亿的整数都适合 JavaScript 数字。这些位还存储负数,因此一个位表示数字的符号。一个更大的问题是,非整数也必须被表示。为此,一些位用于存储小数点的位数。可以存储的实际最大整数更多地处于 9 个四千万亿(15 个零)的范围内,这仍然令人愉悦地庞大。

小数通过使用点来写出。

9.81

对于非常大或非常小的数字,你也可以使用科学计数法,方法是在数字后添加一个“e”(表示“指数”),后面跟着数字的指数

2.998e8

即 2.998 × 108 = 299,800,000。

小于上述 9 个四千万亿的整数的计算保证总是精确的。不幸的是,小数的计算通常不精确。正如 π(圆周率)无法用有限的小数位数精确表示一样,许多数字在只有 64 位可用存储它们时会损失一些精度。这很可惜,但只有在特定情况下才会造成实际问题。重要的是要意识到这一点,并将小数数字视为近似值,而不是精确值。

算术

对数字进行的主要操作是算术。加法或乘法等算术运算接受两个数值,并从中生成一个新的数字。以下是它们在 JavaScript 中的样子

100 + 4 * 11

+* 符号被称为运算符。第一个表示加法,第二个表示乘法。在两个值之间放置一个运算符将把它应用于这些值,并生成一个新值。

示例的意思是“将 4 和 100 相加,并将结果乘以 11”,还是在加法之前进行乘法?正如你可能猜到的,乘法先进行。但就像在数学中一样,你可以通过将加法括起来来改变这一点。

(100 + 4) * 11

对于减法,有 - 运算符,除法可以使用 / 运算符进行。

当运算符一起出现而没有括号时,它们应用的顺序由运算符的优先级决定。示例显示乘法优先于加法。/ 运算符与 * 具有相同的优先级。+- 也是如此。当多个具有相同优先级的运算符彼此相邻出现时,例如在 1 - 2 + 1 中,它们从左到右应用:(1 - 2) + 1

你不应该担心这些优先级规则。如有疑问,只需添加括号。

还有一个算术运算符,你可能不会立即认出来。% 符号用于表示余数运算。X % YX 除以 Y 的余数。例如,314 % 100 生成 14,而 144 % 12 给出 0。余数的优先级与乘法和除法相同。你通常会看到这个运算符被称为模运算,尽管从技术上讲,余数更准确。

特殊数字

JavaScript 中有三个特殊值被认为是数字,但行为不像普通数字。

前两个是 Infinity-Infinity,它们分别表示正无穷大和负无穷大。Infinity - 1 仍然是 Infinity,等等。不要过分相信基于无穷大的计算。它在数学上不严谨,并且会很快导致我们的下一个特殊数字:NaN

NaN 代表“非数字”,即使它是一个数字类型的值。例如,当你尝试计算 0 / 0(零除以零)、Infinity - Infinity 或任何其他无法产生精确、有意义结果的数值运算时,你将得到此结果。

字符串

下一个基本数据类型是字符串。字符串用于表示文本。它们通过用引号括起其内容来写出。

"Patch my boat with chewing gum"
'Monkeys wave goodbye'

只要字符串开头和结尾的引号匹配,单引号和双引号都可以用来标记字符串。

几乎任何东西都可以放在引号之间,JavaScript 会从中生成一个字符串值。但有一些字符更难处理。你可以想象一下,将引号放在引号之间会有多难。换行符(当你按下回车键时得到的字符)也不能放在引号之间。字符串必须保持在单行上。

为了能够将这些字符包含在字符串中,使用了以下符号:每当在带引号的文本中找到一个反斜杠 (\) 时,它就表示它后面的字符具有特殊含义。这被称为转义字符。被反斜杠前缀的引号不会结束字符串,而是成为字符串的一部分。当一个 n 字符出现在反斜杠之后时,它被解释为换行符。类似地,反斜杠后的 t 表示一个制表符。以下面的字符串为例

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

实际包含的文本如下

This is the first line
And this is the second

当然,在某些情况下,你希望字符串中的反斜杠只是一个反斜杠,而不是一个特殊代码。如果两个反斜杠彼此相邻,它们将合并在一起,并且最终的字符串值中只保留一个。这就是如何表达字符串“A newline character is written like "\n".

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

字符串不能被除、乘或减,但 + 运算符可以对它们使用。它不进行加法,而是进行连接——它将两个字符串粘合在一起。以下行将生成字符串 "concatenate"

"con" + "cat" + "e" + "nate"

还有更多操作字符串的方法,我们将在讨论到 第 4 章中的方法时进行讨论。

一元运算符

并非所有运算符都是符号。有些是用文字写成的。一个例子是 typeof 运算符,它生成一个字符串值,命名你给它的值的类型。

console.log(typeof 4.5)
// → number
console.log(typeof "x")
// → string

在示例代码中,我们将使用 console.log 来指示我们想要查看某个表达式的结果。当你运行这样的代码时,生成的数值应该显示在屏幕上,但具体显示方式取决于你用来运行它的 JavaScript 环境。

我们之前看到的其他运算符都是作用于两个值的,而 typeof 运算符只接受一个值。使用两个值的运算符称为 *二元* 运算符,而使用一个值的运算符称为 *一元* 运算符。减号运算符可以既用作二元运算符,也可以用作一元运算符。

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

布尔值

通常情况下,你需要一个仅区分两种可能性的值,例如 "是" 和 "否" 或 "开" 和 "关"。为此,JavaScript 提供了一种名为 *布尔* 的类型,它只有两个值:true 和 false(直接使用这两个单词表示)。

比较

以下是生成布尔值的一种方式。

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

>< 符号分别是 "大于" 和 "小于" 的传统符号。它们是二元运算符。应用它们会生成一个布尔值,指示它们在这种情况下是否成立。

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

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

字符串的排序方式大致上是字母顺序:大写字母始终 "小于" 小写字母,因此 "Z" < "a" 为 true,非字母字符 (!, -, 等等) 也包含在排序中。实际比较基于 *Unicode* 标准。该标准为几乎所有你可能需要的字符都分配了一个数字,包括希腊语、阿拉伯语、日语、泰米尔语等的字符。在计算机内部存储字符串时,拥有这些数字非常有用,因为它可以让它们用数字序列表示。在比较字符串时,JavaScript 会从左到右遍历它们,逐个比较字符的数字代码。

其他类似的运算符有 >=(大于或等于)、<=(小于或等于)、==(等于)和 !=(不等于)。

console.log("Itchy" != "Scratchy")
// → true

在 JavaScript 中,只有一个值不等于它本身,那就是 NaN,它代表 "非数字"。

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

NaN 应该表示无意义计算的结果,因此,它不等于任何 *其他* 无意义计算的结果。

逻辑运算符

还有一些操作可以应用于布尔值本身。JavaScript 支持三种逻辑运算符:*与*、*或* 和 *非*。这些运算符可以用来对布尔值进行 "推理"。

&& 运算符代表逻辑 *与*。它是一个二元运算符,只有当给它提供的两个值都为 true 时,它的结果才为 true。

console.log(true && false)
// → false
console.log(true && true)
// → true

|| 运算符表示逻辑 *或*。如果给它提供的两个值中有一个为 true,它就会生成 true。

console.log(false || true)
// → true
console.log(false || false)
// → false

*非* 写作感叹号 (!)。它是一个一元运算符,用于翻转给它的值——!true 生成 false,而 !false 则生成 true

当将这些布尔运算符与算术运算符和其他运算符混合使用时,并不总是能轻易判断出何时需要括号。在实践中,你通常只需要知道,在我们所见过的运算符中,|| 的优先级最低,其次是 &&,然后是比较运算符 (>==,等等),最后是其他运算符。这种顺序的设定是为了让以下面的表达式为代表的典型表达式尽可能少地使用括号。

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

我将讨论的最后一个逻辑运算符不是一元的,也不是二元的,而是 *三元的*,它作用于三个值。它写作问号和冒号,如下所示

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

这个运算符叫做 *条件* 运算符(有时也简称为 *三元* 运算符,因为它是语言中唯一的这种运算符)。问号左侧的值 "选择" 另外两个值中的哪一个将被输出。当它为 true 时,选择中间的值,当它为 false 时,选择右侧的值。

未定义的值

有两个特殊值,分别写作 nullundefined,用于表示没有意义的值。它们本身是值,但它们不携带任何信息。

语言中许多不会生成有意义值的运算(后面你会看到一些例子)会生成 undefined,仅仅是因为它们必须生成 *某个* 值。

undefinednull 之间的含义差异是 JavaScript 设计中的一个意外,在大多数情况下并不重要。在实际需要关注这些值的情况下,我建议将它们视为可互换的(稍后会详细介绍)。

自动类型转换

在引言中,我提到过,JavaScript 会尽力接受你提供的几乎所有程序,即使是那些做奇怪事情的程序。以下面的表达式为例,很好地说明了这一点。

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

当运算符应用于 "错误" 类型的值时,JavaScript 会默默地将该值转换为它想要的类型,使用一组规则,而这些规则通常不是你想要的或期望的。这被称为 *类型强制*。因此,第一个表达式中的 null 变成了 0,而第二个表达式中的 "5" 变成了 5(从字符串到数字)。然而,在第三个表达式中,+ 在进行数字加法之前尝试字符串连接,因此 1 被转换为 "1"(从数字到字符串)。

当无法明显映射到数字的某些内容(例如 "five"undefined)被转换为数字时,会生成 NaN 值。对 NaN 的进一步算术运算会继续生成 NaN,因此,如果你在一个意料之外的地方发现了一个 NaN,请检查是否发生了意外的类型转换。

当使用 == 比较相同类型的数值时,结果很容易预测:当两个数值相同时,你应该得到 true,除了 NaN 的情况。但是,当类型不同时,JavaScript 会使用一组复杂且令人困惑的规则来确定该怎么做。在大多数情况下,它只是试图将其中一个值转换为另一个值的类型。但是,当 nullundefined 出现于运算符的两侧时,只有当两侧都是 nullundefined 之一时,它才会生成 true。

console.log(null == undefined);
// → true
console.log(null == 0);
// → false

最后一点行为通常很有用。当你想要测试一个值是否具有实际值而不是 nullundefined 时,你可以简单地使用 ==(或 !=)运算符将其与 null 进行比较。

但是,如果你想要测试某个内容是否引用了 false 的精确值呢?将字符串和数字转换为布尔值的规则规定 0NaN 和空字符串 ("") 被视为 false,而所有其他值都被视为 true。因此,0 == false"" == false 这样的表达式也是 true。对于这样的情况,你*不希望*发生任何自动类型转换,有两个额外的运算符:===!==。第一个测试一个值是否精确等于另一个值,第二个测试它是否不精确等于另一个值。因此,"" === false 如预期的那样为 false。

我建议防御性地使用三个字符的比较运算符,以防止意外的类型转换导致你出错。但当你确定两侧的类型都相同的时候,使用较短的运算符是没有问题的。

逻辑运算符的短路求值

逻辑运算符 &&|| 对不同类型的数值以一种奇特的方式处理。它们会将其左侧的值转换为布尔类型,以决定该怎么做,但根据运算符和该转换的结果,它们会返回 *原始* 左侧值或右侧值。

例如,|| 运算符会在其左侧的值可以转换为 true 时返回该值,否则返回其右侧的值。这种转换的效果与你对布尔值的期望相符,它应该对其他类型的数值做类似的事情。

console.log(null || "user")
// → user
console.log("Karl" || "user")
// → Karl

这个功能允许 || 运算符用作一种回退到默认值的方式。如果你给它一个可能在左侧生成空值的表达式,那么右侧的值将用作这种情况下的替代值。

&& 运算符的工作方式类似,但方向相反。当其左侧的值是某些可以转换为 false 的内容时,它会返回该值,否则它会返回其右侧的值。

这两个运算符的另一个重要特性是,它们右侧的表达式只有在必要时才会被求值。对于 true || X,无论 X 是什么——即使它是一个会做 *可怕* 事情的表达式——结果都会是 true,而 X 永远不会被求值。对于 false && X 也是如此,它为 false,并且会忽略 X。这被称为 *短路求值*。

条件运算符的工作方式类似。第一个表达式始终会被求值,但第二个或第三个值(即未被选中的值)不会被求值。

总结

本章我们探讨了 JavaScript 的四种类型的值:数字、字符串、布尔值和未定义值。

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

这足以让你将 JavaScript 用作袖珍计算器,但除此之外就没什么了。下一章将开始将这些表达式组合成基本的程序。