在机器外表以下,程序在运动着。收放自如,毫不费力。电子和谐地散开和重组。显示器上的形状,只不过是水面的涟漪。精髓在不可见的底层。
-- Master Yuan-Ma, The Book of Programming
在计算机的世界里,只有数据。你可以读取数据,修改数据和创建新数据。所有的数据以长长的比特序列的形式存储,所以从根本上来说是相似的。
比特是任一种二值的事物,通常描述为 0 和 1。在计算机中,它们可以是或高或低的电荷、或强或弱的信号、CD表面或明或暗的点等形式。任何离散的信息片段都可以简化成 0 和 1 的序列,因此可以用比特表示。
比如,我们可以用比特来表示数字 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 百京(18 再跟 18 个 0)。这已经很多了。
计算机内存一般要小得多,人们通常用 8 或 16 位的比特组来表示数字。很容易不小心溢出这么小的数字:产生的数字无法以给定数量的比特来表示。现如今,即使是便携式电脑也有大容量的内插,你可以轻松地使用 64 位的块,而且只有当你真正要处理天文数字时才需要考虑溢出的问题。
但是,并不是所有 18 百京以内的整数都能表示为JavaScript
中的数字。还需要保存负数,所以其中 1 个比特表示的是数字的符号。更严重的问题是,非整数也需要表示出来。为了处理这个问题,一些比特被用来保存小数点的位置。可喜的是,实际可保存的最大整数大概是在 9 万亿(15 个 0),仍然非常大。
小数使用“点”来表示:
9.81
对于极大或极小的数字,也可以用科学计数法:添加 1 个e
(表示指数),后面再跟上那个数的指数:
2.998e8
也就是 2.998 × 108 = 299,800,000。
对于上述所说的 9 万亿以内整数(也叫作:整型数)的计算,总是可以保证是精确的。不幸的是,对于小数的计算就通常不是了。正如 π
(pi,圆周率)无法用有限个十进制数字精确表示出来,很多数字在只有 64 个比特可用来保存时会丢失一些精确度。这比较遗憾,不过它只在一些特定的情况下会引发实际的问题。重要的是,要注意到这个问题,并把小数看作是一种近似值,而不是精确值。
数字主要用来做算术。算术运算,比如加或乘,需要 2 个值,并通过它们生成一个新的数字。在JavaScript
中,类似于这样:
100 + 4 * 11
符号 +
和 *
叫作操作符。第一个代表加法,第二个代表乘法。把操作符放在 2 个值中间,会将其作用于那些值,并产生一个新值。
但是,这个例子表示的是“4 与 100 相加,再将结果与 11 相乘”,还是乘法在加法之前运行呢?正如你可能猜的那样,乘法先进行运算。不过,和数学中一样,可以通过把加法放入括号中来改变运算顺序:
(100 + 4) * 11
对于减法,可以用 -
操作符,除法则用 /
操作符来完成。
当有多个操作符而没有使用括号时,运算的顺序由运算符的 优先级 决定。上面的例子说明乘法在加法之前进行。/
操作符和 *
的优先级相同。+
和 -
也是一样的。当多个相同优先级相邻出现时,比如 1 - 2 + 1
,则从左到右执行:(1 - 2) + 1
。
不用过于担心这些优先级的规则。当不确定时,只需要加上括号。
还有一个算术操作符,你可能还不认识。%
符号表示 求余 运算。X % Y
表示 X
被 Y
除所得的余数。例如,314 % 100
结果是 14
,而 144 % 12
则为 0
。求余的优先级与乘除法相同。很多地方也称这个操作符为 求模。
JavaScript
中有 3 个特殊值被看作数字,但与普通数字表现不同。
前两个是 Infinity
和 -Infinity
,分别表示正无穷和负无穷。Infinity - 1
仍然是 Infinity
,以此类推。然而,不要太过相信基于无穷的计算。它在数学上不那么可靠,而且很快就引出下一个特殊数字:NaN
。
NaN
表示“不是数字”,然而它确实 是 一个数字类型的值。当你尝试计算 0 / 0
(0 被 0 相除),Infinity - Infinity
,或任意无法生成有意义结果的数学运算时,将得到这个结果。
下一个基本数据类型是 字符串。字符串用来表示文本。书写时把内容放入引号中:
`Down on the sea`
"Lie on the ocean"
'Float on the ocean'
可以使用单引号、双引号或者反引号来标记字符串,只要引号的开头和结尾匹配即可。
几乎所有内容都可以放进引号中,JavaScript
会相应生产一个字符串值。但对于个别字符来说,就更加困难了。你可以想象一下引号中再放入引号会有多难。换行符 (当敲回车时得到的字符)只有在用反引号(`)引用时可以不用转义。
为了在字符串中包含这样的字符,需要用到如下标记法:在引用的文本中出现反斜杠(\
)时,这表示紧跟着的那个字符具有特殊含义。这叫做 转义 字符。含有反斜杠的引用,不会终止字符串,而是它的一部分。当一个 n
字符出现在反斜杠之后时,表示一个换行符。同样地,反斜杠和 t
组合则表示制表符。考虑如下字符串:
"This is the first line\nAnd this is the second"
其包含的真实文本将是:
This is the first line
And this is the second
当然,也有很多情况:你不想让反斜杠成为特殊字符,就当作一个反斜杠。如果 2 个反斜杠紧挨着,它们会“折叠”在一起:只有一个会留下来出现在最终的字符串值中。比如,字符串 “A newline character is written like "\n".”
可以如下表示:
"A newline character is written like \"\\n\"."
字符串也必须建模成比特串,以供计算机使用。JavaScript
基于 Unicode 标准来做这件事。这个标准会指定一个数字给几乎每一个你需要的字符,包括希腊语、阿拉伯语、日语、亚美尼亚语等等的字符。如果对于每一个字符都有一个数字来对应,那么字符串就可以通过一系列的数字来描述了。
JavaScript
就是这么干的。但是有一个难点:JavaScript
使用共可表示 216 个不同的数字的 16 位比特来表示一个字符串元素。而Unicode定义了更多的字符,差不多 2 倍。因此,有些字符,比如 emoji,在JavaScript
字符串中占 2 个“字符位置”。我们将在第五章重新回到这个话题。
字符串不能相除、相乘或相减,但是 +
操作符却可以用于它们。它做的不是加法,而是 拼接:把两个字符串“粘”在一起。下面一行将生产字符串 "concatenate"
:
"con" + "cat" + "e" + "nate"
字符串有很多相关的函数(方法)用来对其进行其他操作。我们将在第四章继续讨论。
字符串使用单引号或双引号,效果基本一样,唯一的不同在于:在哪一种引号中需要转义。反引号括起来的字符串,通常称为“模板字符串”,有一些额外的技巧。除了能跨行,它们还能嵌入其他值。
`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
很多时候,有一个能区分仅有的两种可能性(比如“yes”和“no”,或“on”和“off”)的值,会非常有用。为此,JavaScript
中有一个 布尔 类型,它只有 2 个值: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
支持 3 种逻辑操作符:与,或 和 非。它们可以被用来对布尔值进行“推理”。
&&
操作符代表逻辑 与。它是一个二元操作符,当且仅当传给它的值都为真时,它的结果才为真。
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
我要讨论的最后一个逻辑操作符,既不是一元的也不是二元的,而是 三元的,作用于 3 个值。它有一个问号和一个冒号,像这样:
console.log(true ? 1 : 2);
// → 1
console.log(false ? 1 : 2);
// → 2
这个操作符叫作 条件操作符(有时候也叫作 三元操作符,因为这是该语言中唯一的此类操作符)。问号左侧的值将“挑选”最终结果是来自于另外两个值中的哪一个。当它为真时,则选中间那个值;为假时,则选右边那个值。
有 2 个特殊值,null
和 undefined
,用来表示“没有意义的值”。它们是值,但却不携带信息。
语言中很多不能生成一个有意义的值的操作(一会儿会看到例子)就会生成 undefined
,因为它们必须生成 某些 值。
null
和 undefined
意义上的不同,是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
的情况除外。当类型不同时,JavaScript
有一套复杂难懂的规则来确定要做什么。大多数情况下,它会尝试把其中一个值转换为另一个值的类型。然而,当 null
或 undefined
出现在操作符的任一边时,当且仅当两边的值同为 null
和 undefined
其中之一时,结果为真。
console.log(null == undefined);
// → true
console.log(null == 0);
// → false
这个行为通常很有用。当你想测试一个值是否真实有值而不是 null
或 undefined
时,就可以把它与 null
用 ==
(或 !=
)操作符来比较。
但是,当你想测试某个值是否指代值 false
时,该怎么办呢?把字符串和数字转换为布尔值的规则表明:0
,NaN
和空字符串(""
)算作 false
,而所有其他值都算作 true
。由此得知,表达式如 0 == false
和 "" == false
均为真。如果你 不 想发生任何自动类型转换,有另外 2 个操作符:===
和 !==
。第一个检查一个值是否 精确 等于另一个值,第二个则检查它是否精确地不等。因此,"" === false
为假,与预期一致。
我建议使用三字符的比较操作符来避免意外的类型转换而导致错误。不过,如果你能确定两边的类型相同,使用短操作符就完全没有问题。
逻辑操作符 &&
和 ||
用特有的方式来处理不同类型的值。它们会把左边的值转换为布尔值,以确定接下来做什么;但是,根据操作符的不同以及转换的结果,它们将要么返回 原始 左边的值或者右边的值。
比如,||
操作符在左值可被转化为真时返回左值,否则返回右值。当值是布尔型时,有着预期的效果,对于其他类型的值也是类似的。
console.log(null || "user")
// → user
console.log("Agnes" || "user")
// → Agnes
我们可以把这个特性作为设置默认值的一种方式。如果你有一个可能为空的值,就可以在其后面加上 ||
和一个替代值。如果原始值可转化为假,将得到那个替代值。
&&
操作符工作方式类似,只不过刚好相反。当左值可转化为假时返回左值,否则返回右值。
这两个操作符的另一个重要特性是:右边的部分只在需要的时候会被求值。对 true || X
来说,无论 X
是什么 —— 即使它是一段 糟糕 的程序,结果都为 true
,而 X
永远不会被求值。同样地,对于 false && X
,结果为 false
并忽略 X
。这叫作 短路求值。
条件操作符也是类似的工作方式。对于第二个和第三个值,只有被选中的那个才会被求值。
本章我们看了JavaScript
值的 4 种类型:数字,字符串,布尔值和未定义的值。
通过键入名称(true
,null
)或值(13
,"abc"
)来创建值。可以用操作符来组合及转换这些值。我们看到了二元操作符来进行算术运算(+
,-
,*
,/
和%
)、字符串拼接(+
)、比较(==
,!=
,===
,!==
,<
,>
,<=
,>=
)和逻辑运算(&&
,||
),还有几个一元操作符(-
对数字取反,!
进行逻辑取反,typeof
查找一个值的类型)和一个三元操作符(?:
)来基于第三个值来挑选两个值中的一个。
这些已经足够让你把JavaScript
当成一个便携计算器来使用了,但也仅此而已。下一章将把这些表达式组合在一起组成基本的程序。