值、类型和运算符

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

元马大师,编程之书
Illustration of a sea of dark and bright dots (bits) with islands in it

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

是任何一种二值事物,通常描述为零和一。在计算机内部,它们采用多种形式,例如高电荷或低电荷,强信号或弱信号,或 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。

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

为了能够处理如此大量的位而不迷失方向,我们将它们分成代表信息片段的块。在 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 的整数进行的计算保证始终是精确的。不幸的是,对小数的计算通常不精确。就像 π(pi)不能用有限的小数位数精确地表示一样,许多数字在只有 64 位可用空间存储它们时会失去一些精度。这很遗憾,但只会在特定情况下造成实际问题。重要的是要意识到这一点,并将小数数字视为近似值,而不是精确值。

算术

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

100 + 4 * 11

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

这个例子是“将 4 和 100 相加,并将结果乘以 11”,还是乘法在加法之前进行?正如你可能已经猜到的,乘法首先进行。与数学一样,你可以通过将加法括在括号中来改变这一点。

(100 + 4) * 11

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

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

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

还有一个算术运算符,你可能不会立即识别它。%符号用于表示余数运算。X % Y是将X除以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}`

当您在模板字面量中编写 ${} 内的内容时,它的结果将被计算,转换为字符串,并在该位置包含。此示例生成字符串 "half of 100 is 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("Garnet" != "Ruby")
// → true
console.log("Pearl" == "Amethyst")
// → 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

这个运算符被称为 *条件* 运算符(有时也称为 *三元运算符*,因为它是语言中唯一一个这样的运算符)。该运算符使用问号左边的值来决定“选择”另外两个值中的哪一个。如果您写 a ? b : c,当 a 为真时,结果将为 b,否则为 c

空值

有两个特殊值,写成 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

当运算符应用于“错误”类型的 value 时,JavaScript 会使用一组规则静默地将该 value 转换为它需要的类型,而这些规则通常不是您想要或期望的。这称为 *类型强制转换*。第一个表达式中的 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 会怎样?像 0 == false"" == false 这样的表达式也为真,因为自动类型转换。当您希望发生任何类型转换时,还有两个额外的运算符:===!==。第一个测试一个值是否精确等于另一个值,第二个测试它是否不精确等于另一个值。因此 "" === false 为假,正如预期的那样。

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

逻辑运算符的短路

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

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

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

我们可以使用此功能作为一种回退到默认值的方法。如果您有一个可能为空的值,可以在它之后加上 ||,并加上一个替换值。如果初始值可以转换为 false,您将获得替换值。将字符串和数字转换为布尔值的规则指出 0NaN 和空字符串 ("") 算作 false,而所有其他值都算作 true。这意味着 0 || -1 生成 -1,而 "" || "!?" 生成 "!?"

?? 运算符类似于 ||,但仅当左侧的值为 nullundefined 时才会返回右侧的值,而不是当它可以转换为 false 的其他值时。

console.log(0 || 100);
// → 100
console.log(0 ?? 100);
// → 0
console.log(null ?? 100);
// → 100

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

这两个运算符的另一个重要属性是,右侧的部分只有在必要时才被评估。在 true || X 的情况下,无论 X 是什么——即使它是一段执行糟糕操作的程序——结果都将为 true,并且 X 永远不会被评估。false && X 也是如此,它为 false 并将忽略 X。这被称为短路评估

条件运算符的工作方式类似。在第二个和第三个值中,只有被选中的一个会被评估。

总结

我们在本章中介绍了四种类型的 JavaScript 值:数字、字符串、布尔值和未定义值。这些值是通过键入它们的名称 (truenull) 或值 (13"abc") 来创建的。

您可以使用运算符组合和转换值。我们看到了用于算术 (+-*/%)、字符串连接 (+)、比较 (==!====!==<><=>=) 和逻辑 (&&||??) 的二元运算符,以及一些一元运算符 (- 用于否定数字,! 用于逻辑否定,typeof 用于查找值的类型) 和一个三元运算符 (?:) 用于根据第三个值选择两个值中的一个。

这为您提供了足够的知识来使用 JavaScript 作为袖珍计算器,但除此之外没有更多内容。下一章将开始将这些表达式串联在一起,形成基本的程序。