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

第 1 章值、类型和运算符

在机器的表面之下,程序在运行。它毫不费力地扩展和收缩。电子和谐地散射和重组。显示器上的形式只是水面的涟漪。本质隐藏在水面之下,不可见。

元马大师,编程之书
Picture of a sea of bits

在计算机的世界里,只有数据。你可以读取数据,修改数据,创建新数据 - 但非数据之物无法被提及。所有这些数据都存储为长长的位序列,因此在本质上是相同的。

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

例如,我们可以用位表示数字 13。它的工作原理与十进制数相同,但你只有 2 个数字,而不是 10 个不同的数字,并且每个数字的权重从右到左增加 2 倍。以下是构成数字 13 的位,其数字权重显示在下面

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

所以这就是二进制数 00001101。它非零的数字代表 8、4 和 1,加起来是 13。

想象一个位的海洋 - 一片无边无际的海洋。一台典型的现代计算机在其易失性数据存储器(工作内存)中拥有超过 300 亿个位。非易失性存储器(硬盘或等效存储器)往往还有几个数量级的更多。

为了能够处理如此大量的位而不会迷失方向,我们必须将它们分成块,以表示信息片段。在 JavaScript 环境中,这些块被称为。虽然所有值都由位构成,但它们扮演着不同的角色。每个值都有一个类型,它决定了它的作用。一些值是数字,一些值是文本片段,一些值是函数,等等。

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

本章介绍了 JavaScript 程序的基本元素,即简单的值类型和可以作用于这些值的运算符。

数字

数字类型的值,不出所料,是数值。在 JavaScript 程序中,它们被写成以下形式

13

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

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

计算机内存过去要小得多,人们倾向于使用 8 位或 16 位的组来表示他们的数字。很容易意外地使这些小数字溢出 - 最终得到一个不适合给定位数的数字。如今,即使是能装进口袋的计算机也有足够的内存,所以你可以自由地使用 64 位的块,并且只有在处理真正天文数字时才需要担心溢出。

然而,并不是所有小于 18 quintillion 的整数都能装进一个 JavaScript 数字。这些位还存储负数,所以一位表示数字的符号。更大的问题是,还需要表示非整数。为此,一些位用于存储小数点的位数。实际可以存储的最大整数更接近 9 quadrillion(15 个零) - 这仍然是一个令人愉快的巨大数字。

小数是用点表示的。

9.81

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

2.998e8

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

小于上述 9 quadrillion 的整数的计算保证始终是精确的。不幸的是,小数的计算一般来说并不精确。就像π(圆周率)不能用有限的小数位精确表示一样,许多数字在只有 64 个位可用存储它们时会失去一些精度。这很遗憾,但它只会在特定情况下造成实际问题。重要的是要意识到这一点,并将小数数字视为近似值,而不是精确值。

算术运算

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

100 + 4 * 11

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

但是这个例子是“将 4 和 100 相加,并将结果乘以 11”,还是先进行乘法再进行加法?正如你可能猜到的,乘法先进行。但正如数学中一样,你可以通过将加法括起来来改变这个顺序。

(100 + 4) * 11

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

当运算符一起出现而没有括号时,它们应用的顺序由运算符的优先级决定。这个例子表明,乘法在加法之前进行。/ 运算符的优先级与 * 相同。+- 也是如此。当多个具有相同优先级的运算符并排出现时,如 1 - 2 + 1,它们从左到右应用:(1 - 2) + 1

这些优先级规则不是你需要担心的事情。如果有疑问,只需添加括号。

还有一个算术运算符,你可能不会立即认出来。% 符号用于表示取余运算。X % YX 除以 Y 的余数。例如,314 % 100 生成 14144 % 12 给出 0。取余运算符的优先级与乘法和除法相同。你也会经常看到这个运算符被称为模运算

特殊数字

在 JavaScript 中有三个特殊值被认为是数字,但它们的行为与普通数字不同。

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

NaN 代表“非数字”,尽管它数字类型的值。例如,当你尝试计算 0 / 0(零除以零)、Infinity - Infinity 或任何其他无法得出有意义结果的数值运算时,你将得到这个结果。

字符串

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

`Down on the sea`
"Lie on the ocean"
'Float on the ocean'

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

几乎任何东西都可以放在引号之间,JavaScript 会把它变成一个字符串值。但是,一些字符更难处理。你可以想象如何在引号之间放置引号会很困难。换行符(当你按下 enter 键时获得的字符)只有在字符串用反引号(`)括起来时才能在不转义的情况下包含在内。

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

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

实际包含的文本是

This is the first line
And this is the second

当然,在某些情况下,你希望字符串中的反斜杠只是一个反斜杠,而不是一个特殊代码。如果两个反斜杠紧挨着,它们将合并在一起,并且在生成的字符串值中只保留一个。这就是如何表达字符串“换行符的写法是 "\n"

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

字符串也必须被建模为一系列位才能在计算机中存在。JavaScript 的实现方式是基于 *Unicode* 标准。该标准为几乎所有你可能需要的字符分配了一个数字,包括来自希腊语、阿拉伯语、日语、亚美尼亚语等的字符。如果每个字符都有一个数字,那么字符串可以用一系列数字来描述。

这就是 JavaScript 的做法。但有一个复杂之处:JavaScript 的表示使用每字符串元素 16 位,最多可以描述 216 个不同的字符。但 Unicode 定义的字符数要比这多得多——目前大约是两倍。因此,一些字符,比如许多表情符号,在 JavaScript 字符串中占据两个“字符位置”。我们将在 第 5 章 中回到这个问题。

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

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

字符串值具有一些关联函数(*方法*),可用于对它们执行其他操作。我将在 第 4 章 中详细介绍这些方法。

用单引号或双引号引起来的字符串的行为非常相似——唯一的区别在于你需要在其中转义哪种引号。反引号引起来的字符串,通常称为 *模板字面量*,可以做一些额外的操作。除了可以跨行外,它们还可以嵌入其他值。

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

当你在模板字面量中写入 ${} 内的东西时,它的结果将被计算,转换为字符串,并包含在该位置。示例生成“100 的一半是 50”。

一元运算符

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

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

我们将在示例代码中使用 console.log 来指示我们希望看到某个东西的求值结果。有关详细信息,请参阅 下一章

上面显示的其他运算符都作用于两个值,但 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",并且非字母字符(!、- 等)也包含在排序中。当比较字符串时,JavaScript 会从左到右逐个比较字符,逐个比较 Unicode 代码。

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

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

在 JavaScript 中,只有一个值不等于自身,那就是 NaN(“非数字”)。

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

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

逻辑运算符

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

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

console.log(true && false)
// → false
console.log(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

这个运算符称为 *条件* 运算符(有时也简称为 *三元* 运算符,因为它是语言中唯一的此类运算符)。问号左边的值“选择”哪一个值会产生。当它为真时,它会选择中间值,而当它为假时,它会选择右边的值。

空值

有两个特殊值,分别写作 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,请查找意外的类型转换。

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

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

这种行为通常很有用。当你想测试某个值是否具有实际值而不是 nullundefined 时,你可以使用 ==(或 !=)运算符将它与 null 进行比较。

但如果你想测试某个东西是否引用的是精确的 false 值呢?像 0 == false"" == false 这样的表达式也是真的,因为有自动类型转换。当你 *不* 希望进行任何类型转换时,有两个额外的运算符:===!==。第一个测试一个值是否 *精确地* 等于另一个值,而第二个测试它是否不精确地等于另一个值。因此,"" === false 如预期的那样为假。

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

逻辑运算符的短路

逻辑运算符 &&|| 对不同类型的值进行处理的方式比较特殊。它们会将左侧的值转换为布尔类型以决定如何处理,但根据运算符和转换结果,它们将返回原始的左侧值或右侧值。

例如,|| 运算符会在左侧的值可以转换为真值时返回左侧的值,否则会返回右侧的值。当值是布尔类型时,这将产生预期的效果,并且对于其他类型的值也会执行类似的操作。

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

我们可以使用此功能作为一种回退到默认值的方式。如果你有一个可能为空的值,你可以在它后面加上 || 和一个替换值。如果初始值可以转换为假值,你将获得替换值。将字符串和数字转换为布尔值的规则规定,0NaN 和空字符串 ("") 算作 false,而所有其他值算作 true。因此,0 || -1 会产生 -1,而 "" || "!?" 会产生 "!?"

&& 运算符的工作原理类似,但方向相反。当左侧的值可以转换为假值时,它会返回该值,否则它会返回右侧的值。

这两个运算符的另一个重要特性是,它们右侧的部分只有在必要时才会被计算。在 true || X 的情况下,无论 X 是什么,即使它是一段执行可怕操作的程序代码,结果也将是真值,并且 X 永远不会被计算。false && X 也是如此,它为假值,并且会忽略 X。这被称为短路求值

条件运算符的工作原理类似。在第二个和第三个值中,只有被选中的那个值会被计算。

总结

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

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

这为你提供了足够的信息,让你可以使用 JavaScript 作为一台袖珍计算器,但除此之外的功能不多。下一章将开始将这些表达式组合在一起,形成简单的程序。