计算机程序通过操作值(如数值3.14)或文本(如"Hello World")来工作。编程语言中这些可以表示和操作的值被称为类型,而一门语言支持的类型集也是这门语言最基本的特征。程序在需要把某个值保存下来以便将来使用时,会把这个值赋给(或存入)变量。变量有名字,程序可以通过这些名字来引用值。变量的工作方式则是一门编程语言的另一个基本特征。本文讲解JavaScript中的类型、值和变量。首先从概念和一些定义开始。
一、概述与定义
JavaScript类型可以分为两类:原始类型和对象类型。JavaScript的原始类型包括数值、文本字符串(也称字符串)和布尔真值(也称布尔值)。本章将用很大篇幅专门详细讲解JavaScript中的数值类型和字符串类型。
JavaScript中的特殊值null和undefined也是原始值,但它们不是数值、字符串或布尔值。这两个值通常被认为是各自特殊类型的唯一成员。ES6新增了一种特殊类型Symbol(符号),用于对语言进行扩展而不破坏向后兼容性。
在JavaScript中,任何不是数值、字符串、布尔值、符号、null和undefined的值都是对象。对象(也就是对象类型的成员)是属性的集合,其中每个属性都有一个名字和一个值(原始值或其他
对象)。有一个非常特殊的对象叫全局对象。
普通JavaScript对象是一个命名值的无序集合。这门语言本身也定义一种特殊对象,称为数组。数组表示一个数字值的有序集合。JavaScript语言包括操作数组的特殊语法,而数组本身也具有区别于普通对象的行为。
除了基本的对象和数组之外,JavaScript还定义了其他一些有用的对象类型。Set对象表示一组值的集合,Map对象表示键与值的映射。各种"定型数组"(typed array)类型便于对字节数组和其他二进制数据进行操作。RegExp类型表示文本模式,可以实现对字符串的复杂匹配、搜索和替换操作。Date类型表示日期和时间,支持基本的日期计算。Error及其子类型表示JavaScript代码运行期间可能发生的错误。
JavaScript与静态语言更大的差别在于,函数和类不仅仅是语言的语法,它们本身就是可以被JavaScript程序操作的值。与其他JavaScript非原始值一样,函数和类也是特殊的对象。
在内存管理方面,JavaScript解释器会执行自动垃圾收集。这意味着JavaScript程序员通常不用关心对象或其他值的析构与释放。当一个值无法触达时,或者说当程序无法以任何方式引用这个
值时,解释器就知道这个值已经用不到了,会自动释放它占用的内存(JavaScript程序员有时候需要留意,不能让某些值在不经意间存续过长时间后仍可触达,从而导致它们无法被回收)。
JavaScript支持面向对象的编程风格。粗略地说,这意味着不用定义全局函数去操作不同类型的值,而是由这些类型本身定义操作值的方法。比如要对数组元素排序,不用把数组传给一个sort()函数,而是可以调用数组a的sort()方法:
JavaScript的对象类型是可修改的(mutable),而它的原始类型是不可修改的(immutable)。可修改类型的值可以改变,比如JavaScript程序可以修改对象属性和数组元素的值。数值、布尔值、符号、null和undefined是不可修改的,以数值为例,修改它是没有意义的。字符串可以看成字符数组,你可能期望它们是可修改的。但在JavaScript中,字符串也是不可修改的。虽然可以按索引访问字符串中的字符,但JavaScript没有提供任何方式去修改已有字符串的字符。
二、数值
JavaScript的主要数值类型Number用于表示整数和近似实数。
当数值真正出现在JavaScript程序中时,就叫作数值字面量(numeric literal)。JavaScript支持几种形式的数值字面量,后面会介绍。注意,任何数值字面量前面都可以加上一个减号(-)将数值变成负值。
2.1、整数字面量
在JavaScript程序中,基数为10的整数可以直接写成数字序列。
除了基数为10的整数字面量之外,JavaScript也支持十六进制(基数是16的)值。十六进制字面量以0x或0X开头,后跟一个十六进制数字字符串。十六进制数字是数字0到9和字母a(或A)到f(或F),a到f表示10到15。
2.2、浮点字面量
浮点字面量可以包含小数点,它们对实数使用传统语法。实数值由数值的整数部分、小数点和数值的小数部分组成。
浮点字面量也可以使用指数记数法表示,即实数值后面可以跟字母e(或E),跟一个可选的加号或减号,再跟一个整数指数。这种记数法表示的是实数值乘以10的指数次幂。
2.3、JavaScript中的算术
JavaScript程序使用语言提供的算术操作符来操作数值,包括表示加法的+、表示减法的-、表示乘法的*、表示除法的/和表示取模(除法后的余数)的%。ES2016增加了取幂的**。
除了上述基本的算术操作符之外,JavaScript还通过Math对象的属性提供了一组函数和常量,以支持更复杂的数学计算:
ES6在Math对象上又定义了一批函数:
JavaScript中的算术在遇到上溢出、下溢出或被零除时不会发生错误。在数值操作的结果超过最大可表示数值时(上溢出),结果是一个特殊的无穷值Infinity。类似地,当某个负数的绝对值超过了最大可表示负数的绝对值时,结果是负无穷值-Infinity。这两个无穷值的行为跟我们的预期一样:任何数加、减、乘、除无穷值结果还是无穷值(只是符号可能相反)。
下溢出发生在数值操作的结果比最小可表示数值更接近0的情况下。此时,JavaScript返回0。如果下溢出来自负数,JavaScript返回一个被称为"负零"的特殊值。这个值与常规的零几乎完全无法区分,JavaScript程序员极少需要检测它。
被零除在JavaScript中不是错误,只会简单地返回无穷或负无穷。不过有一个例外:0除以0是没有意义的值,这个操作的结果是一个特殊的"非数值"(NaN,Not a Number)。此外,无穷除无穷、负数平方根或者用无法转换为数值的非数值作为算术操作符的操作数,结果也都是NaN。
非数值在JavaScript中有一个不同寻常的特性:它与任何值比较都不相等,也不等于自己。这意味着不能通过x === NaN来确定某个变量x的值是NaN。相反,此时必须写成x != x或Number.isNaN(x)。这两个表达式当且仅当x与全局常量NaN具有相同值时才返回true。
全局函数isNaN()与Number.isNaN()类似。它会在参数是NaN时,或者在参数是无法转换为数值的非数值时返回true。相关的函数Number.isFinite()在参数不是NaN、Infinity或-Infinity时返回true。全局isFinite()函数在参数是有限数或者可以转换为有限数时返回true。
2.4、日期和时间
JavaScript为表示和操作与日期及时间相关的数据而定义了简单的Date类。JavaScript的Date是对象,但也有数值表示形式,即自1970年1月1日起至今的毫秒数,也叫时间戳:
三、文本
JavaScript中表示文本的类型是String,即字符串。字符串是16位值的不可修改的有序序列,其中每个值都表示一个Unicode字符。字符串的length属性是它包含的16位值的个数。JavaScript的字符串(以及数组)使用基于零的索引,因此第一个16位值的索引是0,第二个值的索引是1,以此类推。空字符串是长度为0的字符串。JavaScript没有表示单个字符串元素的专门类型。要表示一个16位值,使用长度为1的字符串即可。
3.1、字符串字面量
要在JavaScript程序中包含字符串,可以把字符串放到一对匹配的单引号、双引号或者反引号('、"或`)中。双引号字符和反引号可以出现在由单引号定界的字符串中,同理由双引号和反引号定界的字符串里也可以包含另外两种引号。下面是几个字符串字面量的例子:
使用反引号定界字符串是ES6的特性,允许在字符串字面量中包含(或插入)JavaScript表达式。
JavaScript最早的版本要求字符串字面量必须写在一行,使用+操作符把单行字符串拼接成长字符串的JavaScript代码随处可见。到了ES5,我们可以在每行末尾加一个反斜杠(\)从而把字符串字面量写到多行上。这个反斜杠和它后面的行终结符都不属于字符串字面量。如果需要在单引号或双引号字符串中包含换行符,需要使用字符序列\n。
注意,在使用单引号定界字符串时,必须注意英文中的缩写和所有格,比如can't和O'Reilly中的单引号。因为这里的撇号就是单引号,所以必须使用反斜杠字符(\)"转义"单引号中出现的所有撇号(下一节讲解转义)。
3.2、字符串字面量中的转义序列
反斜杠在JavaScript字符串中有特殊的作用:它与后面的字符组合在一起,可以在字符串中表示一个无法直接表示的字符。例如,\n是一个表示换行符的转义序列。
前面还提到了另一个例子\',表示单引号(或撇号)字符。这种转义序列在以单引号定界字符串时,可以用来在字符串中包含撇号。之所以称之为转义序列,就是反斜杠转换了通常意义上单引号的含义。转义之后,它不再表示字符串结束,而是表示撇号:
JavaScript转义序列:
如果字符\位于任何上表之外的字符前面,则这个反斜杠会被忽略(当然,语言将来的版本有可能定义新转义序列)。例如,\#等同于#。最后,如前所述,ES5允许把反斜杠放在换行符前面从而将一个字符串字面量拆成多行。
3.3、使用字符串
拼接字符串是JavaScript的一个内置特性。如果对数值使用+操作符,那数值会相加。如果对字符串使用+操作符,那字符串会拼接起来(第二个在第一个后面)。例如
可以使用标准的全等===和不全等!==操作符比较字符串。只有当这两个字符串具有完全相同的16位值的序列时才相等。字符串也可以使用<、<=、>和>=操作符来比较。字符串比较是通过比较16位值完成的。
要确定一个字符串的长度(即字符串包含的16位值的个数),可以使用字符串的length属性。
JavaScript还提供了操作字符串的丰富API:
记住,JavaScript中的字符串是不可修改的。像replace()和toUpperCase()这样的方法都返回新字符串,它们并不会修改调用它们的字符串。
字符串也可以被当成只读数组,使用方括号而非charAt()方法访问字符串中个别的字符(16位值)。
3.4、模式匹配
JavaScript定义了一种被称为正则表达式(或RegExp)的数据类型,用于描述和匹配文本中的字符串模式。RegExp不是JavaScript中的基础类型,但具有类似数值和字符串的字面量语法,因此它们有时候看起来像是基础类型。正则表达式字面量的语法很复杂,它们定义的API也没那么简单。由于RegExp很强大,且常用于文本处理,因此本节将简单地介绍一下。
一对斜杠之间的文本构成正则表达式字面量。这对斜杠中的第二个后面也可以跟一个或多个字母,用于修改模式的含义。例如:
RegExp对象定义了一些有用的方法,而字符串也有接收RegExp参数的方法。例如:
四、布尔值
布尔值表示真或假、开或关、是或否。这个类型只有两个值:true和false。
布尔值在JavaScript中通常是比较操作的结果。例如:
以上代码测试变量a的值是否等于数值4。如果是,则返回true;否则返回false。
布尔值在JavaScript常用于控制结构。例如,JavaScript中的if/else语句在布尔值为true时会执行一种操作,而在值为false时会执行另一种操作。我们经常把产生布尔值的比较表达式直接放在使用布尔值的语句中。结果类似如下:
五、null与undefined
null是一个语言关键字,求值为一个特殊值,通常用于表示某个值不存在。对null使用typeof操作符返回字符串"object",表明可以将null看成一种特殊对象,表示"没有对象"。但在实践中,null通常被当作它自己类型的唯一成员,可以用来表示数值、字符串以及对象"没有值"。多数编程语言都有一个与JavaScript的null等价的值,比如NULL、nil或None。
JavaScript中的undefined也表示值不存在,但undefined表示一种更深层次的不存在。具体来说,变量的值未初始化时就是undefined,在查询不存在的对象属性或数组元素时也会得到undefined。另外,没有明确返回值的函数返回的值是undefined,没有传值的函数参数的值也是undefined。undefined是一个预定义的全局常量(而非像null那样的语言关键字,不过在实践中这个区别并不重要),这个常量的初始化值就是undefined。对undefined应用typeof操作符会返回"undefined",表示这个值是该特殊类型的唯一成员。
抛开细微的差别,null和undefined都可以表示某个值不存在,经常被混用。相等操作符==认为它们相等(要区分它们,必须使用全等操作符===)。因为它们俩都是假性值,在需要布尔值的情况下,它们都可以当作false使用。null和undefined都没有属性或方法。事实上,使用.或[]访问这两个值的属性或方法会导致TypeError。
我认为可以用undefined表示一种系统级别、意料之外或类似错误的没有值,可以用null表示程序级别、正常或意料之中的没有值。实际编码中,我会尽量避免使用null和undefined,如果需要给某个变量或属性赋这样一个值,或者需要向函数传入或从函数中返回这样一个值,我通常使用null。有些程序员则极力避免使用null,而倾向于使用undefined。
六、不可修改的原始值与可修改的对象引用
JavaScript中的原始值(undefined、null、布尔值、数值和字符串)与对象(包括数组和函数)有一个本质的区别。原始值是不可修改的,即没有办法改变原始值。对于数值和布尔值,这一点很好理解:修改一个数值的值没什么用。可是,对于字符串,这一点就不太好理解了。因为字符串类似字符数组,我们或许认为可以修改某个索引位置的字符。事实上,JavaScript不允许这么做。所有看起来返回一个修改后字符串的字符串方法,实际上返回的都是一个新字符串。例如:
原始值是按值比较的,即两个值只有在它们的值相同的时候才是相同的。对于数值、布尔值、null和undefined来说,这话听起来确实有点绕。其实很好理解,例如,在比较两个不同的字符串时,当且仅当这两个字符串长度相同并且每个索引的字符也相同时,JavaScript才认为它们相等。
对象不同于原始值,对象是可修改的,即它们的值可以改变:
对象不是按值比较的,两个不同的对象即使拥有完全相同的属性和值,它们也不相等。同样,两个不同的数组,即使每个元素都相同,顺序也相同,它们也不相等:
对象有时候被称作引用类型(reference type),以区别于JavaScript的原始类型。基于这一术语,对象值就是引用,对象是按引用比较的。换句话说,两个对象值当且仅当它们引用同一个底层对象时,才是相等的。
七、类型转换
JavaScript对待自己所需值的类型非常灵活。JavaScript需要一个布尔值,而你可能提供了其他类型的值,JavaScript会根据需要转换这个值。有些值(真性值)转换为true,有些值(假性值)转换为false。对其他类型也是如此:如果JavaScript想要字符串,它就会把你提供的任何值都转换为字符串。如果JavaScript想要数值,它也会尝试把你给的值转换为一个数值(如果无法进行有意义的转换就转换为NaN)。
下表总结了JavaScript中类型之间的转换关系。表中加粗的内容是可能会让人觉得意外的转换目标。空单元格表示没有转换必要,因此什么操作也不会发生。
八、变量声明与赋值
在JavaScript中使用变量或常量前,必须先声明它。在ES6及之后的版本中,这是通过let和const关键字来完成的,接下来我们会介绍。在ES6之前,变量是通过var声明的,这个关键字更特殊一些,将在本节后面介绍。
8.1、使用let和const声明
在现代JavaScript(ES6及之后)中,变量是通过let关键字声明的:
也可以使用一条let语句声明多个变量:
声明变量的同时(如果可能)也为其赋予一个初始值是个好的编程习惯。
如果在let语句中不为变量指定初始值,变量也会被声明,但在被赋值之前它的值是undefined。
要声明常量而非变量,则要使用const而非let。const与let类似,区别在于const必须在声明时初始化常量:
顾名思义,常量的值是不能改变的,尝试给常量重新赋值会抛出TypeError。
声明常量的一个常见(但并非普遍性)的约定是全部字母大写,如H0或HTTP_NOT_FOUND,以区别于变量。
何时使用const:
关于使用const关键字有两种论调。一种论调是只在值基本不会改变的情况下使用const,比如物理常数、程序版本号,或用于标识文件类型的字节序。另一种论调认为程序中很多所谓的变量实际上在程序运行时并不会改变。为此,应该全部使用const声明,然后如果发现确实需要允许值改变,再改成let。这样有助于避免因为意外修改变量而导致出现bug。
8.2、使用var的变量声明
在ES6之前的JavaScript中,声明变量的唯一方式是使用var关键字,无法声明常量。var的语法与let的语法相同:
虽然var和let有相同的语法,但它们也有重要的区别。
- 使用var声明的变量不具有块作用域。这种变量的作用域仅限于包含函数的函数体,无论它们在函数中嵌套的层次有多深。
- 如果在函数体外部使用var,则会声明一个全局变量。但通过var声明的全局变量与通过let声明的全局变量有一个重要区别。通过var声明的全局变量被实现为全局对象的属性。全局对象可以通过globalThis引用。因此,如果你在函数外部写了var x= 2;,就相当于写了globalThis.x = 2;。不过要注意,这么类比并不完全恰当。因为通过全局var创建的这个属性不能使用delete操作符删除。通过let和const声明的全局变量和常量不是全局对象的属性。
- 与通过let声明的变量不同,使用var多次声明同名变量是合法的。而且由于var变量具有函数作用域而不是块作用域,这种重新声明实际上是很常见的。变量i经常用于保存整数值,特别是经常用作for循环的索引变量。在有多个for循环的函数中,每个循环通常都以for(var i = 0; ...开头。因为var并不会把这些变量的作用域限定在循环体内,每次循环都会(无害地)重新声明和重新初始化同一个变量。
- var声明的一个最不同寻常的特性是作用域提升(hoisting)。在使用var声明变量时,该声明会被提高(或提升)到包含函数译注1的顶部。但变量的初始化仍然在代码所在位置完成,只有变量的定义转移到了函数顶部。因此对使用var声明的变量,可以在包含函数内部的任何地方使用而不会报错。如果初始化代码尚未运行,则变量的值可能是undefined,但在初始化之前是可以使用变量而不报错的(这会成为一个bug来源,也是let要纠正的一个最重要的错误特性。如果使用let声明了一个变量,但试图在let语句运行前使用该变量则会导致错误,而不是得到undefined值)。