值、类型和运算符
在机器的表面之下,程序在移动。它毫不费力地扩展和收缩。在和谐中,电子散射和重组。显示器上的形式不过是水面的涟漪。本质无形地潜藏在下方。
在计算机的世界里,只有数据。你可以读取数据,修改数据,创建新数据——但任何不是数据的东西都不能被提及。所有这些数据都存储为长串的位,因此从根本上来说是相同的。
位是任何一种二值事物,通常描述为零和一。在计算机内部,它们采用多种形式,例如高电荷或低电荷,强信号或弱信号,或 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
产生14
,144 % 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
。
空值
有两个特殊值,写成 null
和 undefined
,用于表示没有 *有意义的* 值。它们本身是值,但它们不携带任何信息。
语言中许多不产生有意义值的运算符会产生 undefined
,因为它们必须产生 *某个* 值。
undefined
和 null
之间的含义差异是 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 使用一套复杂且令人困惑的规则来确定要做什么。在大多数情况下,它只是尝试将其中一个值转换为另一个值的类型。但是,当 null
或 undefined
出现在运算符的两侧时,只有当两侧都是 null
或 undefined
之一时,它才会产生 true。
console.log(null == undefined); // → true console.log(null == 0); // → false
这种行为通常很有用。当您想测试一个值是否具有实际值而不是 null
或 undefined
时,可以使用 ==
或 !=
运算符将其与 null
进行比较。
如果您想测试某物是否引用了精确值 false
会怎样?像 0 == false
和 "" == false
这样的表达式也为真,因为自动类型转换。当您不希望发生任何类型转换时,还有两个额外的运算符:===
和 !==
。第一个测试一个值是否精确等于另一个值,第二个测试它是否不精确等于另一个值。因此 "" === false
为假,正如预期的那样。
我建议防御性地使用三个字符的比较运算符,以防止意外的类型转换导致您出错。但是,当您确定两侧的类型都将相同,使用较短的运算符没有问题。
逻辑运算符的短路
逻辑运算符 &&
和 ||
以一种特殊的方式处理不同类型的值。它们将把左侧的值转换为布尔类型以决定要做什么,但是根据运算符和该转换的结果,它们将返回原始左侧值或右侧值。
例如,||
运算符将在左侧的值可以转换为 true 时返回左侧的值,否则将返回右侧的值。当值是布尔值时,这具有预期的效果,并且对其他类型的值执行类似的操作。
console.log(null || "user") // → user console.log("Agnes" || "user") // → Agnes
我们可以使用此功能作为一种回退到默认值的方法。如果您有一个可能为空的值,可以在它之后加上 ||
,并加上一个替换值。如果初始值可以转换为 false,您将获得替换值。将字符串和数字转换为布尔值的规则指出 0
、NaN
和空字符串 (""
) 算作 false,而所有其他值都算作 true。这意味着 0 || -1
生成 -1
,而 "" || "!?"
生成 "!?"
。
??
运算符类似于 ||
,但仅当左侧的值为 null
或 undefined
时才会返回右侧的值,而不是当它可以转换为 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 值:数字、字符串、布尔值和未定义值。这些值是通过键入它们的名称 (true
、null
) 或值 (13
、"abc"
) 来创建的。
您可以使用运算符组合和转换值。我们看到了用于算术 (+
、-
、*
、/
和 %
)、字符串连接 (+
)、比较 (==
、!=
、===
、!==
、<
、>
、<=
、>=
) 和逻辑 (&&
、||
、??
) 的二元运算符,以及一些一元运算符 (-
用于否定数字,!
用于逻辑否定,typeof
用于查找值的类型) 和一个三元运算符 (?:
) 用于根据第三个值选择两个值中的一个。
这为您提供了足够的知识来使用 JavaScript 作为袖珍计算器,但除此之外没有更多内容。下一章将开始将这些表达式串联在一起,形成基本的程序。