19 KiB
第1章 值、类型和运算符
Below the surface of the machine, the program moves. Without effort, it expands and contracts. In great harmony, electrons scatter and regroup. The forms on the monitor are but ripples on the water. The essence stays invisibly below.
Master Yuan-Ma, The Book of Programming
在计算机的世界当中,只有数据。你可以读取、修改以及新建数据,数据可以用来表示任何信息。所有的数据都用类似的方式进行存储:长位(bit)序列。
我们通常使用二值(0和1)来表示位。在计算机中,位可以用一个高电荷或者一个低电荷,一个强信号或者一个弱信号,抑或是光盘表面的凹凸点来表示。任何离散信息都可以简化成由0与1组成的序列,因此这些信息都可以表示成位序列。
举例来说,用位序列来表示数字13。我们可以仿照写十进制数字的方法来写位序列,只是在写位序列的时候用的不是十个不同的数字,而是两个。其中数字的权值以2为因子从右到左依次递增。以下是用位序列来表示的数字13,其中每一位数字下方标注了该位的权值:
0 0 0 0 1 1 0 1
128 64 32 16 8 4 2 1
因此,二进制数00001101,或者说是8+4+1,即等于13。
1.1 值
让我们设想一下包含大量位序列的情况,在一台标准现代计算机的易失性数据存储设备中,就会包含超过300亿位的数据。而非易失性存储设备(硬盘之类的存储设备)则可以存储更多的数据。
为了能够顺利操作如此多的位数据而又不引起数据丢失,我们可以将这些位划分成表示不同信息的块。在JavaScript中,我们将这些数据块称为值。虽然所有的值都是由位序列构成的,但是它们的功能却各不相同。每个值的类型决定了其功能定义。在JavaScript中包含6种基本的值类型:数字(number)、字符串(string)、布尔值(boolean)、对象(object)、函数(function)和未定义类型(undefined)。
创建值的时候,只需要调用其名称即可,非常方便。我们不需要预先了解新创建的值的类型或者为其预先开辟存储空间,只需调用某个值,即可立刻获取到它。当然,这些值也不会凭空产生,每个值总要有一个地方来进行存储,如果你想在同一时间存储海量的值数据,可能就会耗尽内存空间。不过幸运的是,只有你同时需要这么多数据的情况下,才会出现这种问题。如果你不再需要使用某个值了,这个值所对应的数据就会被清理和回收,供其他值来使用。
本章将会介绍JavaScript程序当中的基本元素,包括简单的值类型以及值运算符。
1.2 数字
数字(number)类型的值即数字值。在JavaScript中写成如下形式:
13
在程序中使用这个值的时候,就会将数字13以位序列的方式存放在计算机的内存当中。
JavaScript使用固定长度为64的位序列来存储数字值。我们只能使用64位存储序列产生一定数量的组合,因此可以表示的数字个数也是有限的。对于十进制数来说,如果长度有N位,那么我们可以用其表示10<sup class="calibre3">N</sup>个数字。同理,对于二进制数来说,如果长度有64位,则可以表示2<sup class="calibre3">64</sup>个数字,大约是1800亿亿(18后面跟18个0)个数字,这个数量已经相当大了。
在过去计算机的内存容量要比现在小得多,因此人们一般会使用长度为8或16的位序列来表示数字。在这么小的范围内数字很容易溢出,即给定的位序列无法存储超过这个范围的数字。而现在即便是个人计算机也会配置大量内存,因此我们使用64位数据块来存储数字也不是什么问题,也就是说在处理超大数字的时候也无需担心溢出。
但是在JavaScript中并不可以使用所有小于1800亿亿的数字。这块长度为64的位序列还要能够表示负数,所以需要有一位符号位用来表示数字的正负。还有一个需要我们关注的问题是如何表示非整数,为了实现该功能,还需要使用一些位来存储小数点的位置。因此,在JavaScript中实际可存储的数字范围是1900万亿(9后面跟15个0),这依旧是一个很大的数字。
使用小数点来表示分数。
9.81
对于过大或过小的数字来说,可以使用带e(即exponent,指数)的科学技术法来表示,并在e的后面紧跟该数的指数。
2.998e8
即2.998×10<sup class="calibre3">8</sup>=299,800,000。
当计算小于前文当中提到的9000万亿的整数时,其计算结果会十分精确,不过在计算小数的时候精度却不高。正如(pi)无法使用有限个数的十进制数字表示一样,在使用64位来存储分数时也同样会丢失一些精度。虽说如此,但这类丢失精度只会在一些特殊情况下才会出现问题。因此我们需要注意在处理分数时,将其视为近似值,而非精确值。
1.2.1 算术
与数字密切相关的就是算术。比如,加法或者乘法之类的算术运算会使用两个数值,并产生一个新的数字。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。求余运算符的优先级和乘除法相同。该运算符常常被称为模数运算符,但准确来说还是应该称其为求余运算符。
1.2.2 特殊数字
在JavaScript中有三个特殊的值,它们虽然是数字,但看起来却跟一般的数字不太一样。
前两个是Infinity和-Infinity,它们分别表示正无穷大和负无穷大。Infinity–1的结果仍然是Infinity,以此类推。不要过于依赖无穷运算的结果,因为这类运算并不真正属于数学运算,由此我们可以引出下一个特殊数字:NaN。
虽然NaN是数字类型的值,但我们用其表示“非数值”。举例来说,在计算0/0(0除以0)、Infinity–Infinity或当数值运算中产生不精确或毫无意义的结果时,就用NaN来表示。
1.3 字符串
另一个基本数据类型是字符串,我们使用字符串来表示文本信息。使用引号将内容括起来。
"Patch my boat with chewing gum"
'Monkeys wave goodbye'
其中,单引号和双引号都可以用来标记字符串,只要保证字符串前后引号一致即可。
我们几乎可以在引号中填写任何字符,JavaScript会使用这些字符来生成字符串。但填写有些字符会稍微复杂一些。比如将引号放在引号中就比较麻烦。另外,由于字符串只能放在一行里,所以换行符(输入回车键所产生的字符)也无法放在引号之间。
若要将这些字符存入字符串,需要使用下列规则:当反斜杠(\)出现在引号之间的文本中时,表示紧跟在其后的字符具有特殊含义,我们将其称之为转义符。当引号紧跟在反斜杠后时,并不意味着字符串结束,而表示这个引号是字符串的一部分。当字符n出现在反斜杠后时,JavaScript将其解释成换行符。以此类推,\t表示制表符,我们来看看下面这个字符串:
"This is the first line\nAnd this is the second"
该字符串实际表示的文本是:
This is the first line
And this is the second
当然了,有时你希望字符串中的反斜杠就表示反斜杠本身,而非特殊字符。你可以使用两个连续的反斜杠来表示一个反斜杠,最后的字符串值中也只会留下一个反斜杠。因此,字符串“A newline character is written like"\n"”可以写成:
"A newline character is written like \"\\n\"."
我们不能将除法、乘法或减法运算符用于字符串,但是“+”运算符却可以。这种情况下,运算符并不表示加法,而是连接操作:将两个字符串连接到一起。以下语句可以产生字符串“concatenate”:
"con" + "cat" + "e" + "nate"
还有很多其他方式来操作字符串,我们会在第4章中进行讨论。
1.4 一元运算符
并非所有的运算符都是用符号来表示,还有一些运算符是用单词表示的。比如typeof运算符,会产生一个字符串的值,内容是给定值的具体类型。
console.log(typeof 4.5)
// → number
console.log(typeof "x")
// → string
我们会在示例代码中使用console.log命令来打印并表示我们需要查看的一些运算结果。当你执行了这段代码后,屏幕上就会显示出运算产生的值,而如何显示结果则取决于执行程序所使用的JavaScript环境。
我们所见过的绝大多数运算符都使用两个值进行操作,而typeof仅接受一个值进行操作。使用两个值的运算符称为二元运算符,而使用一个值的则称为一元运算符。减号运算符既可用作一元运算符,也可用作二元运算符。
console.log(- (10 - 2))
// → -8
1.5 布尔值
我们常常需要用一个值来简单地区分两种可能性,比如说“是”和“否”以及“开”和“关”。JavaScript使用布尔类型来表示这种情况,该类型的值只有两种取值:true和false(就用这两个英文单词来表示)。
1.5.1 比较
一种产生布尔值的方法如下所示:
console.log(3 > 2)
// → true
console.log(3 < 2)
// → false
“>”和“<”符号分别表示“大于”和“小于”。这两个符号是二元运算符,通过该运算符返回的结果是一个布尔值,表示其运算是否为真。
我们可以使用相同的方法比较字符串。
console.log("Aardvark" < "Zoroaster")
// → true
字符串的比较是按照字母顺序来进行比较的,大写字母总是“小于”小写字母,因此"Z"<"a"的结果为真,另外字符顺序中也包括了非字母字符(比如!、–等)。实际上字符的比较是基于Unicode标准实现的。该标准为你需要的每个字符赋予了一个数字,包括希腊文、阿拉伯文、日文和泰米尔文等。使用这些数字有助于将字符串存储在计算机中,因为这样我们就可以将字符与数字一一对应,并将字符串存储成数字的序列了。在比较字符串时,JavaScript从左向右逐个比较每个字符对应的数字编码。
其他类似的运算符则包括>=(大于等于)、<=(小于等于)、==(等于)和!=(不等于)。
console.log("Itchy" != "Scratchy")
// → true
在JavaScript中,只有一个值不等于其自身,那就是NaN(Not a Number,非数值)。
console.log(NaN == NaN)
// → false
NaN用于表示非法运算的结果,正因如此,不同的非法运算结果也不会相等。
1.5.2 逻辑运算符
还有一些运算符可以应用于布尔值上。JavaScript支持三种逻辑运算符:与(and)、或(or)和非(not)。这些运算符可以用于推理布尔值。
&&运算符表示逻辑与,该运算符是二元运算符,只有当赋给它的两个值均为true时其结果才是真。
console.log(true && false)
// → false
console.log(true && 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
该运算符被称为条件运算符(或者是三元运算符,因为JavaScript语言中只有这唯一的一个三元运算符)。条件运算符根据问号左侧条件的真伪,从其余两个值中挑选出一个作为结果。当条件为真时选择中间的值,当条件为假时选择右侧的值。
1.6 未定义值
这里有两个特殊的值,分别是null和undefined,用于表示无意义的值。它们各自表示其自身含义,除此之外不包含任何信息。
在JavaScript语言中,有许多操作都会产生无意义的值(我们会在后面的内容中看到实例),这些操作会得到undefined的结果仅仅只是因为每个操作都必须产生一个值。
JavaScript语言设计上的问题导致了undefined和null含义存在些许不同,在绝大部分情况下这种区别无关痛痒。如果你遇到了不得不区分这两个值的特殊情况,那么我的建议是将这两个值视作可互换的值(稍后对其进行详细介绍)。
1.7 自动类型转换
在本书的开篇,我曾提到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这种情况,只要两个值相同,则返回true。但如果类型不同,JavaScript则会使用一套复杂难懂的规则来确定输出结果。在绝大多数情况下,JavaScript只是将其中一个值转换成另一个值的类型。但如果运算符两侧存在null或undefined,那么只有两侧均为null或undefined时结果才为true。
console.log(null == undefined);
// → true
console.log(null == 0);
// → false
上文提及的最后一点其实非常有用。当我们想确定一个值是否为真,而非null或undefined时,直接使用“==”(或“!=”)运算符来进行比较即可。
但当你想要测试一个值是否严格等于false时会发生什么呢?字符串与数字的布尔类型转换规则是:JavaScript会将0、NaN和空字符串("")视为false,其他值视为true。因此,诸如0==false和""==false之类的表达式都是true。在这种情况下,如果我们不希望在比较的时候进行任何自动类型转换,可以使用另外两个运算符:===和!==。第一个运算符用于检测两个值是否严格相等,第二个运算符用于测试是否严格不等。所以""===false的结果是false,正如我们预期。
我建议使用三字符比较运算符来防止意外类型转换的发生,避免作茧自缚。但如果比较运算符两侧的值类型是相同的,那么使用较短的运算符也没有问题。
逻辑运算符的短路特性
逻辑运算符&&和||可以使用一种特殊方式来处理不同类型的值。这两个运算符会将左侧的值转换成布尔类型,以决定如何进行后续操作,但返回左侧值还是返回右侧值,则取决于运算符和左侧转换结果。
举例来说,当左侧值可以被转换成true时,||运算符会直接返回左侧的值,否则会返回右侧的值。当你希望以布尔值的方式来处理其他类型的值时,这种转换就派上用场了。
console.log(null || "user")
// → user
console.log("Karl" || "user")
// → Karl
||运算符的这种功能可用于返回默认值。如果左侧表达式可能产生空值,那么右侧的表达式可以在左侧表达式为空时作为替代。
&&运算符工作方式与其相似但不相同。当左侧的值可以被转换成false时,&&运算符会返回左侧值,否则返回右侧值。
这两种运算符的另一个重要特性是:只有必要时才会计算右侧的表达式。以true||X为例,无论X是什么,即使X会进行一些可怕操作,其结果都是true,而且永远都不会计算X。false&&X这种情况也是一样,只要左侧是false,X就会被忽略。这被称为短路计算。
条件运算符也会以类似方式工作。第一个表达式总会被计算,但第二个或第三个表达式只有在被选择时才会被计算。
1.8 本章小结
在本章中,我们介绍了JavaScript的四种类型的值:数字、字符串、布尔值和未定义值。
通过输入值的名称(true、null)或值(13、"abc")就可以创建它们。你还可以通过运算符来对值进行合并和转换操作。本章已经介绍了算术二元运算符(+、–、*、/和%)、字符串连接符(+)、比较运算符(==、!=、===、!==、<、>、<=和>=)、逻辑运算符(&&和||)和一些一元运算符(–表示负数,!表示逻辑非,typeof用于查询值的类型)。
利用这些知识,你完全可以编写出一个迷你计算器了,但仅限于此。在下一章中,我们准备将这些表达式应用在基本的程序当中。