在计算机的世界里,只有数据。你可以读取数据、修改数据、创建新数据,但不能提及非数据的内容。所有这些数据都存储为长位序列,因此本质上是相似的。
位(比特)是任何类型的二值事物,通常被描述为0和1。在计算机内部,它们的形式包括高电荷或低电荷、强信号或弱信号、或者 CD 表面上的闪亮或暗点。任何离散信息都可以简化为由 0 和 1 组成的序列,从而以位表示。
例如,我们可以用位来表示数字13。这与十进制数的工作方式相同,但我们只有 2 个数字,而不是 10 个不同的数字,并且每个数字的权重从右到左增加 2 倍。以下是组成数字 13 的位,其下方显示了数字的权重:
这就是二进制数 00001101。它的非零数字分别代表 8、4 和 1,加起来是 13。
值
想象一下比特的海洋——比特的海洋。典型的现代计算机在其易失性数据存储(工作内存)中拥有超过 1000 亿位。非易失性存储(硬盘或同等存储设备)往往要高出几个数量级。
为了能够处理如此数量的位而不丢失,我们将它们分成代表信息片段的块。在 JavaScript 环境中,这些块称为值。尽管所有值都是由位组成的,但它们扮演着不同的角色。每个值都有一个决定其角色的类型。有些值是数字,有些值是文本片段,有些值是函数,等等。
要创建一个值,你只需调用它的名称即可。这很方便。你不必为自己的价值观收集建筑材料或为其付费。你只要呼唤一个,嗖嗖地,你就得到了。当然,价值并不是凭空产生的。每一个都必须存储在某个地方,如果你想同时使用大量它们,你可能会耗尽计算机内存。幸运的是,只有当你同时需要它们时,这才是一个问题。一旦你不再使用某个值,它就会消散,留下其碎片,作为下一代值的建筑材料进行回收。
本章的其余部分介绍 JavaScript 程序的原子元素,即简单值类型和可以作用于这些值的运算符。
数值(NUMBER)
不难理解,数字类型的值就是数值。在 JavaScript 程序中,它们的写法如下:
在程序中使用它,计算机内存中就会出现数字 13 的位型。
JavaScript 使用固定的 64 位来存储单个数字值。64 位只能有这么多的模式,这就限制了可以表示的不同数字的数量。用 N 个十进制数可以表示 10^N 个数字。同样,给定 64 个二进制位,可以表示 2^64 个不同的数字,大约是 18 万亿(18 后面有 18 个零)。这是一个很大的数字。
过去,计算机内存要小得多,人们倾向于使用 8 或 16 位组来表示数字。这样小的数字很容易意外溢出,最终得到的数字不适合给定的位数。如今,即使是放在口袋里的电脑也有足够的内存,所以你可以自由地使用 64 位组,只有在处理真正的天文数字时才需要担心溢出问题。
不过,并不是所有小于 18500 亿的整数都适合 JavaScript 数字。这些位还可以存储负数,因此有一位表示数字的符号。更大的问题是如何表示非整数。为此,一些位被用来存储小数点的位置。实际可存储的最大整数大约在 9 夸脱亿(15 个零)的范围内--这仍然是一个令人愉快的巨大数字。
小数用点来书写:
对于非常大或非常小的数字,你还可以使用科学记数法,添加 e(代表指数),后跟数字的指数。
就是 2.998 × 108 = 299,800,000
使用小于上述 9 夸脱亿的整数(也称为整数)进行计算,可以保证始终精确。遗憾的是,小数的计算通常并不精确。就像π(圆周率)不能用有限的十进制数精确表示一样,当只有 64 位可用来存储时,许多数字都会失去一些精度。这是一个遗憾,但它只会在特定情况下造成实际问题。重要的是要意识到这一点,并将小数数字视为近似值,而不是精确值。
算术
与数字有关的主要是算术运算。算术运算(如加法或乘法)将两个数值相加,然后产生一个新的数字。下面是 JavaScript 中的算术运算:
+ 和 * 符号称为运算符。前者表示加法,后者表示乘法。将运算符放在两个值之间,就会对这两个值进行运算,并产生一个新值。这个例子的意思是 “把 4 和 100 相加,然后把结果乘以 11”,还是先做乘法再做加法?你可能已经猜到了,乘法是先做的。在数学中,可以用括号将加法包起来来改变这种情况。
对于减法,有 - 运算符。除法可以使用 / 运算符。
当运算符不带括号一起出现时,它们的应用顺序由运算符的优先级决定。本例中,乘法运算优先于加法运算。运算符 / 的优先级与 * 相同。同样,+ 和 - 的优先级相同。当多个具有相同优先级的运算符相邻出现时,如 1 - 2 + 1,它们的应用顺序是从左到右:(1 - 2) + 1。不用太在意这些优先规则。如有疑问,只需加上括号即可。
还有一个算术运算符,你可能无法立即识别。% 符号用来表示余数运算。例如,314 % 100 表示 14,144 % 12 表示 0。余数运算符的优先级与乘法和除法相同。你还会经常看到这个运算符被称为 modulo。
特殊值
在 JavaScript 中,有三个特殊值被视为数字,但它们的行为与普通数字不同。前两个是 Infinity 和 -Infinity,分别代表正无穷大和负无穷大。无穷大-1 仍然是无穷大,以此类推。不过,不要太相信基于无穷大的计算。这在数学上并不靠谱,而且很快就会产生下一个特殊数字: NaN。
NaN 表示 “不是数字”,尽管它是一个数字类型的值。例如,当你尝试计算 0 / 0(零除以零)、Infinity - Infinity 或任何其他不产生有意义结果的数值运算时,都会得到这样的结果。
字符串
下一种基本数据类型是字符串。
字符串用于表示文本。在书写时,用引号将其内容括起来。
你可以使用单引号、双引号或回车键来标记字符串,只要字符串开头和结尾的引号匹配即可。
你几乎可以把任何东西放在引号之间,让 JavaScript 将其生成一个字符串值。但是有几个字符就比较困难了。你可以想象在引号之间加引号有多难,因为它们看起来就像字符串的结尾。换行符(按回车键时出现的字符)只有在字符串的引号中加了反斜线 (\) 时才能包含。
为了能在字符串中包含这些字符,使用了以下符号:引号内的反斜杠(\)表示后面的字符具有特殊含义。这就是所谓的转义字符。引号前的反斜杠不会结束字符串,而是字符串的一部分。当 n 字符出现在反斜线之后时,会被解释为换行符。同样,反斜线后的 t 表示制表符。以下面的字符串为例
这是字符串中的实际文本:
当然,在某些情况下,你希望字符串中的反斜线只是一个反斜线,而不是一个特殊代码。如果两个反斜线紧跟在一起,它们就会折叠在一起,结果字符串值中只剩下一个反斜线。这就是字符串 "换行符的写法是"\n". " 的表达方式:
字符串也必须作为一系列比特来建模,才能在计算机中存在。JavaScript 采用的方法是基于 Unicode 标准。该标准为你需要的几乎所有字符都分配了一个数字,包括希腊文、阿拉伯文、日文、亚美尼亚文等等。如果每个字符都有一个数字,那么一个字符串就可以用一串数字来描述。这就是 JavaScript 所要做的。
但有一个复杂的问题: JavaScript 的表示方法是每个字符串元素使用 16 位,最多可以描述 216 个不同的字符。然而,Unicode 定义的字符比这多得多,目前大约是其两倍。因此,有些字符(如许多表情符号)在 JavaScript 字符串中占据了两个 “字符位置”。我们将在第 5 章讨论这个问题。
字符串不能被除、乘或减。在字符串上可以使用 + 运算符,但不是用来相加,而是用来连接--将两个字符串粘合在一起。下面一行将产生字符串 “concatenate”:
字符串值有许多相关函数(方法),可用于对它们执行其他操作。我将在第 4 章详细介绍这些函数。使用单引号或双引号编写的字符串的行为非常相似,唯一的区别在于需要在字符串内部转义哪种类型的引号。反引号字符串(通常称为模板字符串)还可以玩一些小把戏。除了可以跨行外,它们还可以嵌入其他值。
当你在模板文字中的 ${} 内写入内容时,其结果将被计算、转换为字符串并包含在该位置。本例将生成字符串 “100 的一半是 50”。
一元运算符
并非所有运算符都是符号。有些运算符写成单词。其中一个例子是 typeof 操作符,它会产生一个字符串值,并命名所给值的类型。
我们将在示例代码中使用 console.log 来表示我们想查看某个内容的评估结果。(下一章将详细介绍)。
本章到目前为止所展示的其他操作符都对两个值进行操作,但 typeof 只取一个值。使用两个值的运算符称为二元运算符,使用一个值的运算符称为一元运算符。减号运算符 (-) 既可以用作二元运算符,也可以用作一元运算符。
布尔值
通常情况下,我们需要一个值来区分两种可能性,比如 “是 ”和 “否”,或者 “开 ”和 “关”。为此,JavaScript 提供了一种布尔类型,它只有两个值,即 true 和 false,并用这两个词来表示。
比较
下面是一种生成布尔值的方法:
> 和 < 符号分别是 “大于 ”和 “小于 ”的传统符号。它们是二元运算符。应用它们的结果是一个布尔值,表示在这种情况下它们是否成立。
字符串也可以用同样的方法进行比较。
字符串的排序方式大致按字母顺序排列,但与字典中的排序方式不同:大写字母总是比小写字母 “少”,因此 “Z”<“a”,而且非字母字符(!、- 等)也包含在排序中。在比较字符串时,JavaScript 会从左到右逐个比较字符的 Unicode 代码。其他类似的运算符有 >=(大于或等于)、<=(小于或等于)、==(等于)和 !=(不等于)。
在 JavaScript 中,只有一个值不等于自身,那就是 NaN(“非数字”)。
NaN 应该表示无意义计算的结果,因此,它不等于任何其他无意义计算的结果。
逻辑运算符
还有一些操作可以应用于布尔值本身。JavaScript 支持三种逻辑运算符:and、or 和 not。这些操作可以用来对布尔值进行 “推理”。
- && 运算符表示逻辑和。它是一个二进制运算符,只有给定的两个值都为真时,其结果才为真。
- 操作符 || 表示逻辑或。如果给定的任一值为真,它就会产生 “真”。
- Not 的写法是感叹号(!)。它是一个一元运算符,可以翻转给定的值!true 产生 false,!false 产生 true。
在将这些布尔运算符与算术运算符和其他运算符混合使用时,需要使用括号的情况并不总是很明显。在实践中,你通常可以知道,在我们迄今为止看到的运算符中,|| 的优先级最低,然后是 &&,然后是比较运算符(>、== 等),然后是其他运算符。选择这样的顺序,是为了在像下面这样的典型表达式中,尽可能少地使用括号:
我们要学习的最后一个逻辑运算符不是一元运算符,也不是二元运算符,而是三元运算符,对三个值进行运算。它用问号和冒号书写,就像这样:
这种运算符称为条件运算符(有时也称为三元运算符,因为它是该语言中唯一的此类运算符)。该运算符使用问号左边的值来决定 “选择 ”其他两个值中的哪一个。如果写 a ? b : c,当 a 为真时,结果将是 b,否则将是 c。
空值
有两个特殊值,分别写为 null 和 undefined,用来表示没有有意义的值。它们本身就是值,但不携带任何信息。语言中的许多操作都不会产生有意义的值,产生 undefined 的原因很简单,因为它们必须产生一些值。
undefined和null在意义上的区别只是JavaScript设计中的一个意外,在大多数情况下并不重要。如果你确实需要关注这些值,我建议你将它们视为可以互换的值。
自动类型转换
在引言中,我提到过 JavaScript 会不遗余力地接受你给它的几乎任何程序,甚至是那些做奇怪事情的程序。下面的表达式就很好地证明了这一点:
当运算符被应用于 “错误 ”类型的值时,JavaScript 会使用一系列规则将该值悄悄转换为所需的类型,而这些规则往往不是你想要或期望的。这就是所谓的类型强制。
第一个表达式中的 null 变成了 0,第二个表达式中的 “5 ”变成了 5(从字符串转换为数字)。然而,在第三个表达式中,“+”会在数字加法之前尝试字符串连接,因此 “1 ”被转换为 “1”(从数字到字符串)。当一些不能以明显方式映射到数字的内容(如 “5 ”或未定义)被转换为数字时,就会得到 NaN 值。对 NaN 的进一步算术运算会不断产生 NaN,所以如果你发现自己在一个意想不到的地方得到了一个这样的值,请查找意外的类型转换。
当使用 == 运算符比较相同类型的值时,结果很容易预测:当两个值相同时,应该得到 true,NaN 的情况除外。但当类型不同时,JavaScript 会使用一套复杂而混乱的规则来决定如何处理。在大多数情况下,JavaScript 会尝试将其中一个值转换为另一个值的类型。但是,当运算符两边都出现 null 或 undefined 时,只有两边都是 null 或 undefined 时,才会产生 true。
我建议谨慎地使用三个字符的比较运算符(===),以防止意外的类型转换绊倒你。但是,如果你确定双方的类型相同,使用较短的操作符就没有问题了。
逻辑运算符短路
逻辑运算符 && 和 || 以一种特殊的方式处理不同类型的值。它们会将左侧的值转换为布尔类型,以决定做什么,但根据运算符和转换结果,它们会返回左侧的原始值或右侧的值。
例如,当 || 运算符的左侧值可以转换为 true 时,它将返回该值,否则将返回右侧值。当数值是布尔值时,这种操作会产生预期的效果,而对于其他类型的数值,这种操作也会产生类似的效果。
我们可以利用这一功能来使用默认值。如果有一个值可能是空的,可以在它后面加上 || 替换值。如果初始值可以转换为 false,那么就会得到替换值。将字符串和数字转换为布尔值的规则规定,0、NaN 和空字符串(“”)为假,所有其他值为真。这意味着 0 || -1 产生-1,而 “” || “!?” 产生 “!?” 。
操作符“?? ”类似于“||”,但只有当左边的值为 null 或undefined时,才会返回右边的值,而不会返回可转换为 false 的其他值。通常,这比 || 的行为更可取。
&&运算符的工作原理与此类似,但正好相反。当左边的值转换为 false 时,返回该值,否则返回右边的值。
这两个运算符的另一个重要特性是,只有在必要时才对其右边的部分进行求值。在 true || X 的情况下,无论 X 是什么,即使它是一段做了可怕事情的程序,结果都是 true,而 X 从未被求值。假 && X 的情况也是如此,它是假的,将忽略 X。
条件运算符的工作方式与此类似。在第二和第三个值中,只有被选中的那个才会被求值。
总结
在本章中,我们介绍了 JavaScript 值的四种类型:数字、字符串、布尔值和未定义值。这些值通过输入名称(true、null)或值(13、“abc”)来创建。
你可以使用运算符组合和转换值。我们看到了用于算术(+、-、*、/和%)、字符串连接(+)、比较(==、!=、===、!==、<、>、<=、>=)和逻辑(&&、||、??)的二元运算符,以及几个一元运算符(-用于否定数字,!用于逻辑否定,typeof用于查找值的类型)和一个三元运算符(?:),用于根据第三个值从两个值中选择一个。
这些信息足以让你把 JavaScript 用作袖珍计算器,但也仅此而已。下一章将开始把这些表达式整合到基本程序中。