第 1 章值、类型和运算符
在机器的表面之下,程序在运行。它毫不费力地扩展和收缩。电子和谐地散射和重组。显示器上的形式只是水面的涟漪。本质隐藏在水面之下,不可见。
在计算机的世界里,只有数据。你可以读取数据,修改数据,创建新数据 - 但非数据之物无法被提及。所有这些数据都存储为长长的位序列,因此在本质上是相同的。
位是任何类型的二进制事物,通常被描述为零和一。在计算机内部,它们以不同的形式存在,例如高电荷或低电荷,强信号或弱信号,或 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 % 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}`
当你在模板字面量中写入 ${}
内的东西时,它的结果将被计算,转换为字符串,并包含在该位置。示例生成“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
这个运算符称为 *条件* 运算符(有时也简称为 *三元* 运算符,因为它是语言中唯一的此类运算符)。问号左边的值“选择”哪一个值会产生。当它为真时,它会选择中间值,而当它为假时,它会选择右边的值。
空值
有两个特殊值,分别写作 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
当将运算符应用于“错误”类型的值时,JavaScript 会静静地将该值转换为它需要的类型,使用一组规则,这些规则通常不是你想要或期望的。这称为 *类型强制*。第一个表达式中的 null
变为 0
,而第二个表达式中的 "5"
变为 5
(从字符串到数字)。然而,在第三个表达式中,+
在数值加法之前尝试字符串连接,因此 1
被转换为 "1"
(从数字到字符串)。
当将无法明显映射到数字的东西(例如 "five"
或 undefined
)转换为数字时,你将获得 NaN
值。对 NaN
的进一步算术运算会继续产生 NaN
,因此,如果你在意外的地方发现了一个 NaN
,请查找意外的类型转换。
当使用 ==
比较相同类型的值时,结果很容易预测:当两个值相同时,你应该得到真,除了 NaN
的情况。但当类型不同时,JavaScript 会使用一组复杂且混乱的规则来确定该怎么做。在大多数情况下,它只会尝试将一个值转换为另一个值的类型。但是,当 null
或 undefined
出现在运算符的两侧时,只有当两侧都是 null
或 undefined
之一时,它才会产生真。
console.log(null == undefined); // → true console.log(null == 0); // → false
这种行为通常很有用。当你想测试某个值是否具有实际值而不是 null
或 undefined
时,你可以使用 ==
(或 !=
)运算符将它与 null
进行比较。
但如果你想测试某个东西是否引用的是精确的 false
值呢?像 0 == false
和 "" == false
这样的表达式也是真的,因为有自动类型转换。当你 *不* 希望进行任何类型转换时,有两个额外的运算符:===
和 !==
。第一个测试一个值是否 *精确地* 等于另一个值,而第二个测试它是否不精确地等于另一个值。因此,"" === false
如预期的那样为假。
我建议防御性地使用三个字符的比较运算符,以防止意外的类型转换导致错误。但当你可以确定两侧的类型都将相同时,使用较短的运算符就没有问题。
逻辑运算符的短路
逻辑运算符 &&
和 ||
对不同类型的值进行处理的方式比较特殊。它们会将左侧的值转换为布尔类型以决定如何处理,但根据运算符和转换结果,它们将返回原始的左侧值或右侧值。
例如,||
运算符会在左侧的值可以转换为真值时返回左侧的值,否则会返回右侧的值。当值是布尔类型时,这将产生预期的效果,并且对于其他类型的值也会执行类似的操作。
console.log(null || "user") // → user console.log("Agnes" || "user") // → Agnes
我们可以使用此功能作为一种回退到默认值的方式。如果你有一个可能为空的值,你可以在它后面加上 ||
和一个替换值。如果初始值可以转换为假值,你将获得替换值。将字符串和数字转换为布尔值的规则规定,0
、NaN
和空字符串 (""
) 算作 false
,而所有其他值算作 true
。因此,0 || -1
会产生 -1
,而 "" || "!?"
会产生 "!?"
。
&&
运算符的工作原理类似,但方向相反。当左侧的值可以转换为假值时,它会返回该值,否则它会返回右侧的值。
这两个运算符的另一个重要特性是,它们右侧的部分只有在必要时才会被计算。在 true || X
的情况下,无论 X
是什么,即使它是一段执行可怕操作的程序代码,结果也将是真值,并且 X
永远不会被计算。false && X
也是如此,它为假值,并且会忽略 X
。这被称为短路求值。
条件运算符的工作原理类似。在第二个和第三个值中,只有被选中的那个值会被计算。
总结
我们在本章中介绍了四种类型的 JavaScript 值:数字、字符串、布尔值和未定义值。
这些值通过输入其名称 (true
、null
) 或值 (13
、"abc"
) 来创建。你可以使用运算符组合和转换值。我们看到了用于算术运算 (+
、-
、*
、/
和 %
)、字符串连接 (+
)、比较运算 (==
、!=
、===
、!==
、<
、>
、<=
、>=
) 和逻辑运算 (&&
、||
) 的二元运算符,以及几个一元运算符 (-
用于对数字取反,!
用于对逻辑取反,typeof
用于查找值的类型) 和一个三元运算符 (?:
) 用于根据第三个值选择两个值之一。
这为你提供了足够的信息,让你可以使用 JavaScript 作为一台袖珍计算器,但除此之外的功能不多。下一章将开始将这些表达式组合在一起,形成简单的程序。