JavaScript高级程序设计-3-基本概念1-数据类型

记录js语言的基本概念的笔记、包括语法和流控制语句,分析js与其他基于C的语言在语法上的相同和不同之处,与内置操作符有关的类型转换问题。

语法

ES的语法大量借鉴了C及其他类C语言的语法,因此,熟悉语法的开发人员在接受ES更宽松的语法时会觉得很简单轻松。

  • 区分大小写,ES的一切(包括变量名、函数名、操作符)都是区分大小写的
  • 标识符,惯例按照小驼峰格式,如myCar,firstList,不能将保留字,关键字,true/false/null作为标识符
  • 严格模式,ES5中引入的严格模式(strict mode)定义了不同于ES3的解析和执行模型。该模式纠正了一些ES3中不确定的行为,对某些不安全的操作会抛出错误。
    在脚本中添加"use strict";之后的代码将执行严格模式,这行代码看起来像字符串,而且没有赋值给任何变量,其实是一个编译指示(pragma),告诉js引擎切换到严格模式,这是为了不破坏ES3语法而特定选定的语法。
  • 语句,ES中的语句以分号结尾,若省略分号,则由解析器确定语句的结尾。不省略分号能够避免很多错误(如不完整的输入),同时利于压缩,在某些情况下能增加代码的性能,因为解析器不必再花时间推测应该在哪里插入分号。
    代码块以花括号{}为界,虽然在条件控制语句(如if、for等)下只执行一行代码时能省略花括号,但最佳实践是始终写花括号,因为让代码更清晰,降低修改代码出错的几率

关键字和保留字

JS关键字和保留字

变量

ES的变量是松散类型的,即可以用来保存任何类型的数据,将每个变量看作是一个用于保存值的占位符即可。定义变量时使用var操作符(var是一个关键字),后面跟一个变量名(即标识符)。

使用var操作符定义的变量将成为定义该变量的作用域中的局部变量,也就是说,当在函数中使用var定义了一个变量,那么这个变量将在函数退出后被销毁,而未经初始化的变量会保存一个特殊的值undefined

若省略var操作符则会定一个全局变量,但不推荐这样定义全局变量,在严格模式下,省略var操作符会报错,给一个未经声明的变量赋值在严格模式下会导致ReferenceError的错误,同时非常不利于维护,也容易导致一些混乱。

严格模式下,不能定义名为eval和arguments的变量(虽然他们俩并不是保留字或关键字)。

可以在一条语句内定义多个变量, 用逗号分开,初始化是可选的:

1
2
3
var msg = 'hi',
find = false,
age = 11;

数据类型

ES定义了5中简单数据类型(基本数据类型):Undefined,Null,Boolean,Nu,mber,String。以及一种复杂类型Object,其本质是无序的键值对集。ES不支持创建自定义类型,所有的值最终都是上述6种数据类型之一。但由于ES数据类型具有动态性,因此没有定义其他类型的必要。

typeof操作符

鉴于ES变量是松散类型,因此需要有一种手段来检测给定变量的数据类型,typeof就是干这个的,对一个值使用typeof操作符可能返回如下的字符串:

  • "undefined" 若这个值未定义
  • "boolean" 若这个值是布尔值
  • "string" 若这个值是字符串
  • "number" 若这个值是数字
  • "object" 若这个值是对象或null
  • "function" 若这个值是函数
    typeof是一个操作符,不是函数,虽然可以用括号将其后跟的操作数包裹起来,操作数可以是变量,也可以是数值字面量。

从技术角度来说,ES中的函数是一个对象,而不是一种数据类型,这个对象有一些特殊的属性,因此通过typeof操作符区分函数和其他对象是有必要的。

注:因为特殊值null被认为是一个空对象引用,所以typeof null返回"object"

Undefined类型

Undefined类型只有一个值,即undefined。在使用var声明变量但未对其加以初始化时这个变量的值就是undefined。

一般而言不需要显示将一个变量设置为undefined,字面量值undefined主要目的是用于比较。

但是包含undefined值的变量与未定义的变量还是不一样:

1
2
3
4
var msg; // 这个变量声明后默认为undefined值

alert(msg); // "undefined"
alert(age); // 没有声明,产生错误

对于一个未声明的变量,只能进行一个操作,那就是使用typeof检测其数据类型(对未声明的变量调用delete在普通模式下虽然不报错但没有意义,同时严格模式还是会报错的)。同时需要注意的是,对未初始化的变量执行typeof会返回"undefined"值,与未声明的变量执行一样。即:

1
2
var msg;
typeof msg === typeof age; // true

综上,显示初始化变量是一个好的选择,即当typeof操作符返回"undefined"的时候,就知道被检测变量没声明而不是没初始化。

Null类型

Null类型也是只有一个值的数据类型,即null。从逻辑上来说,null值表示一个空对象指针,所以typeof检测null返回object是合理的。

若定义的变量准备在将来用于保存对象,那么最好将变量初始化为null而不是其他值,这样只要对比null值就知道是否已经保存了一个对象的引用:

1
2
3
if(car !== null){
// ...
}

而undefined值派生自null,所以两者比较返回true(涉及到数据类型的隐式转换),但用全等比较时则返回false,因为全等首先比较就是数据类型

1
2
null == undefined; // true
null === undefined; // false

虽然null和undefined的关系有些混乱,但记着他们的用途不用就行了。即无论如何,没有必要将一个变量的值显示设置为undefined,但对null却没有这个规则。换句话说,只要意在保存对象的变量还没有真正保存对象时,就应该让其保存null,这样不仅能体现null作为空对象指针的惯例,也有助于进一步区分null和undefined。

Boolean类型

虽然Boolean类型只有两个字面量值:true和false,但其是用的最多的类型。注意:true不一定等于1,false也不一定等于0。同时他们是区分大小写的。

由于ES的隐式转换规则,所有类型的值都有与这两个Boolean值等价的值。也可以显示调用转换函数Boolean()将任意一个值转换为Boolean值:

1
Boolean('hello'); // true

转换规则如下(需要特别注意数据的隐式转换):

数据类型 转换为true的值 转换为false的值
String 任何非空字符串 ""(空字符串)
Number 任何非零数字值(包括无穷) 0和NaN
Object 任何对象 null
Undefined 不适用此规则 undefined

Number类型

ES中的Number类型使用IEEE754标准表示整数和浮点数(双精度数值),ECMA-262定义了不同的数值字面量格式。

  • 十进制,默认的格式,

    1
    var intNum = 55; // 整数
  • 八进制,第一位必须是0(零),然后是八进制数字序列(0 ~ 7),若字面量值中的数字超出了范围,则忽略前导0,后面的数值按十进制解析:

    1
    2
    3
    var octalNum1 = 070; // 八进制,解析为十进制 56
    var octalNum2 = 079; // 无效八进制,解析为十进制 79
    var octalNum3 = 08; // 无效八进制,解析为十进制 8

    在严格模式下,八进制是无效的,会报错

  • 十六进制,前两位必须是0x(零x),后跟任何十六进制数字(0 ~ 9,A ~ F / a ~ f),其中字母大小写皆可。
    1
    2
    var hexNum1 = 0xA; // 十六进制,解析为十进制 10
    var hexNum2 = 0x1f; // 十六进制,解析为十进制 31

在进行算术运算时,所有以八进制或十六进制表示的数值最终都将被转换成十进制数值。

浮点数值

浮点数即数值中必须包含一个小数点,并且至少有1未小数,小数点前的0可以省略,但不推荐。

1
2
3
var floatNum1 = 1.1;
var floatNum2 = 0.1;
var floatNum3 = .1; // 有效,不推荐

由于保存浮点数值需要的内存空间是保存整数值的两倍,因此ES会尽量将浮点数值转换为整数值,即当小数点后没有数字(1.)或本身就是一个整数时(1.0),那么就被转换为整数。

对于极大或极小的数值,可以用科学计数法表示。虽然浮点数值最高精度是17位小数,但在进行算术计算时精确度远不如整数,小小的舍入误差会导致无法比较特定的浮点数值:

1
2
3
4
5
if(a + b == 0.3){
// 0.15 + 0.15 没问题
// 0.1 + 0.2 会出错
// 所以不要比较某个特定的浮点数值
}

浮点数的舍入误差是由于采用IEEE754标准的问题,其他基于IEEE754计算浮点数的语言也会出现这种问题。

数值范围

由于内存限制,ES所能表示的最小的数值在变量Number.MIN_VALUE中,最大值在变量Number.MAX_VALUE中,若某次计算的结果超出了js的数值范围,则这个数值将会自动转换成特殊的Infinity值,具体来说,若是负数则转换为-Infinity(负无穷)

若某次计算的结果为正/负无穷,则无法参加下一次的计算,因为Infinity不是一个可以进行计算的数值。要想确定一个数值是不是有穷的,可以使用isFinite()方法,当数值位于最小和最大的数值之间时返回true。

1
2
var result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false

NaN

NaN,即非数值(Not a Number),是一个特殊的值,这个数组用于表示一个本来要返回数值的操作数未返回数值的情况(不会报错),比如任何数值除以0都会出错,其他语言一般会报错,代码停止执行,但是js不会,只会返回NaN,这样不会影响到其他代码的执行。

NaN本身有两个特点:

  • 任何涉及NaN的计算操作都会返回NaN,
  • NaN和任何数都不想等,包括它自己。

针对上述特点,ES定义了isNaN()函数来确定参数是否“不是数值”,它在接收到一个值后,会尝试将这个值转换为数值(比如字符串"10"或Boolean值true),若不能转换为数值则会导致这个函数返回true。

1
2
3
4
5
isNaN(NaN); // true
isNaN(10); // false, 10是一个数值
isNaN("10"); // false, 能被转换为数值10
isNaN("blue"); // true, 不能转换为数值
isNaN(true); // false,能被转换为数值1

特别注意:isNaN能被用于对象,当对一个对象调用isNaN函数时,首先会调用对象的valueOf()方法,然后确定该方法返回的值是否可以转换为数值,若不能,则再基于这个返回值再调用toString()方法,在测试toString()返回的值。这个流程也是ES的内置函数和操作符流程。

数值转换

有3个函数可以把非数值转换为数值:Number(),parseInt()和parseFloat(),第一个函数,可以用于任何数据类型,其他两个函数专用于将字符串转换为数值,这3个函数对于同样的输入会返回不同的结果:
Number()函数的转换规则如下:

  • 若是Boolean值,true转换为1,false转换为0
  • 若是数值值,则直接返回
  • 若是null值,则返回0
  • 若是undefined值,则返回NaN
  • 若是字符串,则规则如下:
    1. 若字符串只包含数字(包括带正号和负号的情况),则将其转换为十进制数值(前导零会被忽略)
    2. 若字符串包含有效的浮点格式(如"1.1"),则转换为对应的浮点数值(同样忽略前导零)
    3. 若字符串包含有效的十六进制格式(如"0xf"),则转换为相同大小的十进制整数
    4. 若字符串是空,则转换为0
    5. 若字符串不满足上述规则,则转换为NaN
  • 若是对象,则转换规则与isNaN相似,首先会调用对象的valueOf()方法,然后将valueOf()方法返回的值依据上述规则转换为数值,若结果为NaN,则再调用对象的toString()方法,然后再将toString()返回的值按照前面的规则转换为字符串值。

由于Number()函数在转换字符串时比较复杂而且不够合理,所以在处理整数的时候更常用的是parseInt函数。

parseInt在转换字符串时,主要看其是否符合数值模式,它会忽略字符串前面的空格,直至找到第一个非空字符。若第一个字符不是数字字符或负号,则返回NaN,也就是说,parseInt转换空字符串会返回NaN(而Number则返回0)。若第一个字符时数字字符,则继续解析第二个字符,直到解析完所有字符或遇到一个非数字字符。例如:"1234ac"会转换为1234,blue会被忽略,"22.11"会转为22,因为小数点不是有效的数字字符。

需要注意的是,parseInt能转换各种进制的整数格式,若以“0x”开头,则当做十六进制解析。若字符串的以“0”开头,理论上则会当做八进制解析,但ES3和ES5对八进制解析有区别,ES3认为是八进制,而ES5将不具有解析八进制的能力,即parseInt("070")会转换为0。

而为了消除上述的困惑,parseInt函数提供第二个参数,转换基数(即进制),默认为10进制, 同时参数一可以省略表示进制的前导符,但由于js的实现,不指定基数则由实现自行决定转换规则,所以无论如何要指定基数,即使是十进制。

1
2
3
4
5
6
7
parseInt('0xAF', 16); // 175
parseInt('AF', 16); // 175
parseInt('AF'); // NaN
parseInt("10", 2); // 2 二进制解析,转换为十进制得到 2
parseInt("10", 8); // 2 八进制解析,转换为十进制得到 8
parseInt("10", 10); // 2 十进制解析 10
parseInt("10", 16); // 2 十六进制解析,转换为十进制得到 16

与parseInt类似,parseFloat则将字符串解析为浮点数,依次解析每一个字符,直至第一个无效的浮点数字符,即字符串中的第一个小数点有效,而第二个小数点则无效,其后面的字符也将被忽略。如22.13.1将转换为22.13

parseFloat将始终忽略前导0,而十六进制的字符串则会解析为0,同时,若字符串解析的结果可转换为整数,则parseInt会返回整数:

1
2
3
4
5
6
parseFloat("1234Float"); // 1234 整数
parseFloat("0xA"); // 0
parseFloat("22.5"); // 22.5
parseFloat("22.13.1"); // 22.13
parseFloat("0901.2"); // 901.2
parseFloat("1.1e3"); // 1100

String类型

String类型即字符串,由零个或多个16位Unicode字符组成的字符序列。字符串可以选择使用单引号或双引号表示。

字符字面量

String数据类型包含一些特殊的字符字面量,即转义序列,用于表示非打印字符或其他用途的字符,例如

  • \n 换行
  • \t 制表
  • \b 空格
  • \r 回车
  • \\ 斜杠
  • \xnn 以十六进制代码nn表示一个字符,n为0 ~ f,例如\x41表示"A"
  • \unnnn 以十六进制代码nnnn表示一个Unicode字符,n为0 ~ f,例如\u03a3表示希腊字符Σ
    还有很多其他转义序列,这些转义序列可以出现在字符串中任意位置,同时也作为一个字符来解析,即只占1个字符的大小,即若字符串中包含双字节字符,那么length属性可能不会精确返回字符串的字符数目。
字符串特点

ES中的字符串是不可变的,即字符串一旦创建,值就不可变。要修改某个变量保持的字符串中的某个字符,必须销毁原来的字符串,然后再用另一个新字符串填空该变量。

转换为字符串

将一个值转换为字符串有两种方式,第一种是每个值都会有的toString方法,这个方法唯一作用就是返回相应值的字符串表示。

1
2
3
4
var age = 11;
age.toString(); // 字符串 "11"
var found = true;
age.toString(); // 字符串 "1true"

数值、布尔值、对象和字符串(每个字符串的toString方法返回该字符串的一个副本)都有toString方法,但nullundefined没有该方法。

多数情况下,toString不用参数,但当调用一个数值的toString方法时,可以传递一个参数,表示基数,默认为10,这个基数只要是任意有效进制格式即可:

1
2
3
4
5
6
var num = 10;
num.toString(); // "10"
num.toString(2); // "1010"
num.toString(8); // "12"
num.toString(10); // "10"
num.toString(16); // "a"

第二种就是转型函数String(),该函数按照toSting的规则转换:但遇到nullundefined时分别返回字符串"null""undefined"

还有一个简单的方法就是使用加号操作符,将它与空字符串相加,即可获得一个字符串值了。

Object类型

ES中的对象其实就是一组数据和功能的集合,对象可以通过执行new操作符后跟要创建的对象类型的名称来创建,而创建Object类型的实例并为其添加属性或方法,就可以创建自定义对象,如下:

1
var o = new Object();

与java语法类似,Object其实是一个构造函数,而在ES中,若不给构造函数传递参数,则可以省略后面的一对圆括号,即上面的实例代码与下面语句等价:

1
var o = new Object; // 有效,但不推荐省略圆括号

仅仅是创建Object实例没什么用处,关键是理解一个思想:在ES中,Object的构造函数是其实例的基础,即Object类型所具有的任何属性和方法也同样存在于更具体的对象中,而每个Object的实例对象都具有下列属性和方法:

  • Constructor 属性,构造函数,保存着用于创建当前对象的函数,对于前面的例子,构造函数就是Object()
  • hasOwnProperty(propertyName) 方法,用于检查给定属性在当前对象实例中(而不是实例的原型中)是否存在。其中,作为参数的属性名propertyName必须以字符串形式给出,即o.hasOwnProperty("name");
  • isPropertyOf(object) 方法,用于检查传入的对象是否是另一个对象的原型
  • propertyIsEnumerable(propertyName) 方法,用于检查给定属性是否能够使用for-in语句来枚举,与hasOwnProperty方法一样,作为参数的属性名必须是字符串。
  • toLocalString() 方法,返回对象的字符串表示,该字符串与执行环境的地区有关
  • toString() 方法,返回对象的字符串表示
  • valueOf() 方法,返回对象的字符串、数值或布尔值表示,通常与toString方法的返回值相同。

ES中的Object是所有对象的继承,隐藏所有对象都具有这些基本的属性和方法,后续将深入学习。同时由于ECMA-262定义的对象行为不一定适用于所有js的其他对象,比如浏览器中的对象,BOM、DOM中的对象都属于宿主对象,因此由宿主实现的宿主对象可能不会继承Object。