张啸


世界上最快乐的事,莫过于为理想而奋斗。


JS(9) 类型、值和变量

计算机程序的运行需要对值(value)进行操作。在编程语言中,能够表示并操作的值的类型称作数据类型(type),编程语言最基本的特性就是能够支持多种数据类型。当程序需要将值保存起来以备将来使用时,便将其赋值给一个变量(variable)。变量是一个值的符号名称,可以通过名称来获得对值的引用。变量的工作机制是编程语言的另一个基本特性。

JavaScript的数据类型分为两类:原始类型(primitive type)和对象类型(object type)。JavaScript中的原始类型包括数字、字符串和布尔值。

JavaScript中有两个特殊的原始值:null(空)和undefined(未定义),它们不是数字字符串布尔值。它们通常分别代表了各自特殊类型的唯一的成员。

JavaScript中除了以上类型之外就是对象了(ES6中新增了Symbol类型)。对象(object)是属性(property)的集合,每个属性都由“名/值对”(值可以是原始值,比如数字、字符串,也可以是对象)构成。其中有一个比较特殊的对象——全局对象(global object)。

JavaScript的类型可以分为原始类型和对象类型,也可分为可以拥有方法的类型和不能拥有方法的类型,同样可分为可变类型(mutable)和不可变类型(immutable)。可变类型的值是可修改的,对象和数组属于可变类型:JavaScript程序可以更改对象属性值和数组元素的值。数字布尔值nullundefined属于不可变类型——比如,修改一个数值的内容本身就说不通。字符串可以堪称是由字符组成的数组,然而在JavaScript中,字符串是不可变的:可以访问字符串任意位置的文本,但JavaScript并为提供修改已知字符串的文本内容的方法。

JavaScript可以自由地进行数据类型转换,比如,如果在程序期望使用字符串的地方使用了数字,JavaScript会自动将数字转换为字符串。如果在期望使用布尔值的地方使用了非布尔值,程序也会进行相应的转换。


一、数字

和其他编程语言不同,JavaScript不区分整数值和浮点数值。JavaScript中所有数字均用浮点数值表示。JavaScript采用IEEE 754标准定义的64位浮点格式表示数字,这意味着它能表示的最大值是±1.7976931348623157 * 10e308,最小值是±5 * 10e-324

按照JavaScript中的数字格式,能够表示的整数范围是从-2*53 ~ 2*53,包含边界值。如果使用了超过此范围的整数,则无法保证低位数字的精度。然而需要注意的是,JavaScript中实际的操作(比如数字索引),则是基于32位整数。

1. 整型直接量

在JavaScript程序中,用一个数字序列表示一个十进制整数,例如

1
2
3
0
3
100000

除了十进制的整型直接量,JavaScript同样能识别八进制、十六进制值。

1
2
0xff;                           // 15*16+15=255
0377; // 3*64+7*8+7=255

最好不要使用以0为前缀的整型直接量,毕竟我们无法得知当前JavaScript的实现是否支持八进制的解析。在ES6的严格模式下,八进制直接量是明令禁止的。

2. 浮点型直接量

浮点型直接量可以含有小数点,它们采用的是传统的实数写法。一个实数由整数部分、小数点和小数部分组成。

1
2
3
4
3.14
2345.789
6.02e23
1.4738223E-32

3. JavaScript中的算术运算

JavaScript程序是使用语言本身提供的算术运算符来进行数字运算的。这些运算符包括+-*/%

除了基本的运算符外,JavaScript还支持更加复杂的算术运算,这些复杂运算通过作为Math对象的属性定义的函数和常量来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Math.pow(2, 53);                        // => 9007199254740992: 2e53
Math.round(.6); // => 1.0: 四舍五入
Math.ceil(.6); // => 1.0: 向上取整
Math.floor(.6); // => 0.0: 向下取整
Math.abs(-5); // => 5: 求绝对值
Math.max(x, y, z); // 返回最大值
Math.min(x, y, z); // 返回最小值
Math.random(); // 生成一个大于等于0小于1.0的伪随机数
Math.PI; // π: 圆周率
Math.E; // e: 自然对数的底数
Math.sqrt(3); // 3的平方根
Math.pow(3, 1/3); // 3的立方根
Math.sin(0); // 三角函数,还有Math.cos,Math.atan等
Math.log(10); // 10的自然对数
Math.log(100)/Math.LN10; // 以10为底100的对数
Math.log(512)/Math.LN2; // 以2为底512的对数
Math.exp(3); // e的三次幂

JavaScript中的算术运算在溢出(overflow)、下溢(underflow)或被零整除时不会报错。当数字运算结果超过了JavaScript所能表示的数字上限,结果为一个特殊的无穷大值(infinity),在JavaScript中以Infinity表示,负无穷大以-Infinity表示。无穷大值的行为特征和我们所期望的是一致的:基于它们的加减乘除运算结果还是无穷大值。

下溢(underflow)是当运算结果无限接近于零并比JavaScript所能表示的最小值还小的时候发生的情形。这种情况下,JavaScript会返回0。当一个负数发生下溢时,JavaScript返回一个特殊的值“负零”。这个值几乎和正常的零完全一样,JavaScript程序中很少用到负零。

被零整出在JavaScript中并不报错:它只是简单地返回无穷大或负无穷大。但有一个例外,零除以零是没有意义的,这种整除运算结果也是一个非数字值(not-a-number, NaN)。无穷大除以无穷大,给任意负数作开方运算或者算术运算符不是数字或无法转换为数字的操作数一起使用时都将会返回NaN。

JavaScript中的非数字值有一点特殊:它和任何值都不想等,包括自身。也就是说,没办法通过x==NaN来判断变量x是否是NaN。相反,应当使用x!=x来判断,当前仅当x为NaN的时候,表达式的结果才为true。函数isNaN()的作用与此类似,如果参数是NaN或者是一个非数字值(比如字符串和对象),则返回true。JavaScript有一个类似的函数isFinite(),在参数不是NaN、Inifity或-Infinity的时候返回true。

负零值同样有些特殊,它和正零值是相等的(甚至使用===严格相等来判断)。

这意味着这两个值几乎一模一样,除了作为除数之外。

1
2
3
4
var zero = 0;
var negz = -0;
zero === negz; // true 正零值和负零值相等
1/zero === 1/negz; // false 正无穷大和负无穷大不等

二、文本

字符串(string)是一组由16位值组成的不可变的有序序列,每个自负通常来自于Unicode字符集。JavaScript通过字符串类型来表示文本。字符串的长度(length)是其所含16位值的个数。JavaScript字符串(和其数组)的索引从零开始:第一个字符的位置是0,第二个字符的位置是1,以此类推。空字符串(empty string)长度为0,JavaScript中并没有表示单个字符的“字符型”。要表示一个16位值,只须将其赋值给字符串变量即可,这个字符串长度为1。

1. 字符串直接量

在JavaScript程序中的字符串直接量,是由单引号或双引号括起来的字符序列。由单引号定界的字符串中可以包含双引号,由双引号定界的字符串中也可以包含单引号。

1
2
3
4
5
"";
'testing';
"3.14";
'name="myform"';
"Wouldn't you prefer O'Reilly's book?";

在ES3中,字符串直接量必须写在一行中,而在ES5中,字符串直接量可以拆分成数行,每行必须以反斜线(\)结束,反斜线和行结束符都不算是字符串直接量的内容。

1
2
3
4
"two\nlines";
"one\
long\
line" // 用三行代码定义了显示为单行的字符串,在ES5中可用

2. 字符串的使用

JavaScript的内置功能之一就是字符串连接。如果将加号(+)运算符用于数字,表示两数相加。但将它用于字符串,则表示字符串连接,将第二个字符串拼接在第一个之后。

1
2
msg = 'Hello, ' + 'world';              // 'Hello, world'
greeting = "Welcome to my blog, " + ' ' + name;

要确定一个字符串的长度——其所包含的16位值的个数——可以使用字符串的length属性。

1
s.length;

除了length属性,字符串还提供许多可以调用的方法

1
2
3
4
5
6
7
8
9
10
11
12
var s = 'hello, world';
s.charAt(0); // 'h'
s.charAt(s.length-1); // 'd'
s.substring(1, 4); // 'ell'
s.slice(1, 4); // 'ell'
s.slice(-3); // 'rld'
s.indexOf('l'); // 2
s.lastIndexOf('l'); // 10
s.indexOf('l', 3); // 3
s.split(', '); // ['hello', 'world']
s.replace('h', 'H'); // 'Hello, world'
s.toUpperCase(); // 'HELLO, WORLD'

记住,在JavaScript中字符串是固定不变的,类似replace()toUpperCase()的方法都返回新字符串,原字符串本身并没有发生改变。

在ES5中,字符串可以当作只读数组,除了使用charAt()方法,也可以使用方括号来访问字符串中的单个字符(16位值)

1
2
3
s = 'hello, world';
s[0]; // 'h'
s[s.length - 1]; // 'd'

三、布尔值

布尔值指代真或假、开或关、是或否。这个类型只有两个值,保留字true和false。

任意JavaScript的值都可以转换为布尔值,下面这些值会被转换成false

undefined null 0 -0 NaN ""

所有其他值,包括所有对象(数组)都会转成true。


四、null和undefined

null是JavaScript语言的关键字,它表示一个特殊值,常用来描述“空值”。对null执行typeof运算,结果返回字符串“object”,也就是说,可以将null认为是一个特殊的对象值,含义是“非对象”。但实际上,通常认为null是它自有类型的唯一一个成员,它可以表示数字、字符串和对象是“无值”的。

JavaScript还有第二个值来表示值的空缺,用未定义的值表示更深层次的“空值”。在ES3中,undefined是可读/写的变量,可以给它赋任意值。这个错误在ES5中做了修正,undefined在该版本中是只读的。如果使用typeof运算符得到undefined的类型,则返回“undefined”,表明这个值是这个类型的唯一成员。


五、全局对象

全局对象(global object)在JavaScript中有着重要的用途:全局对象的属性是全局定义的符号,JavaScript程序可以直接使用。当JavaScript解释器启动时,它将创建一个新的全局对象,并给它一组定义的初始属性:

  • 全局属性,比如undefinedInfinityNaN

  • 全局函数,比如isNaN()parseInt()eval()

  • 构造函数:比如Date()RegExp()String()Object()Array()

  • 全局对象,比如MathJSON

在代码的最顶级——不在任何函数内的JavaScript代码——可以使用JavaScript关键字this来引用全局对象。


六、包装对象

JavaScript对象是一种复合值:它是属性或已命名值的集合。通过“.”符号来引用属性值。当属性值是一个函数的时候,称其为方法。我们看到字符串也同样具有属性和方法

1
2
var s = 'hello world!';
var word = s.substring(s.indexOf(' ') + 1, s.length);

字符串既然不是对象,为什么它会有属性呢?只要引用了字符串s的属性,JavaScript就会将字符串值通过调用new String(s)的方式转换成对象,这个对象继承了字符串的方法,并被用来处理属性的引用。一旦属性引用结束,这个新创建的对象就会销毁。

同字符串一样,数字和布尔值也具有各自的方法:通过Number()Boolean()构造函数创建一个临时对象,这些方法的调用均是来自于这个临时对象。nullundefined没有包装对象:访问它们的属性会造成一个类型错误。

1
2
3
var s = 'test';
s.len = 4;
var t = s.len;

运行上述代码时,t的值是undefined。第二行代码创建一个临时字符串对象,并给其len属性赋值为4,随即销毁这个对象。第三行通过原始的字符串值创建一个新字符串对象,尝试读取其len属性,这个属性自然不存在,表达式求值结果为undefined。这段代码说明了在读取字符串、数字和布尔值的属性值(或方法)的时候,表现的和对象一样。但如果试图给其属性赋值,则会忽略这个操作:修改只是发生在临时对象上,而这个临时对象并未继续保留下来。

存取字符串、数字或布尔值的属性时创建的临时对象称作包装对象。它只是偶尔用来区分字符串值和字符串对象、数字和数值对象以及布尔值和布尔对象。

需要注意的是,可通过String()Number()Boolean()构造函数来显式创建包装对象

1
2
3
4
var s = 'test', n = 1, b = true;
var S = new String(s);
var N = new Number(n);
var B = new Boolean(b);

其中“==”运算符时,原始值与其包装对象相等,“===”全等运算符时,它们不等。通过typeof运算符可以看到原始值和其包装对象的不同。


七、不可变的原始值和可变的对象引用

JavaScript中的原始值(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着本质区别。原始值是不可更改的:任何方法都无法更改一个原始值。对字符串来说不那么容易理解,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来修改字符串中的字符。实际上,JavaScript是禁止这样做的,字符串所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。

1
2
3
var s = 'hello';
s.toUpperCase(); // 'HELLO'
s; // 'hello'

原始值的比较是值的比较:只有在它们的值相等时它们才相等。这对数字、布尔值、null和undefined来说听起来有点难懂,并没有其他办法来比较它们。同样,对于字符串来说则不明显:如果比较两个单独的字符串,当且仅当它们的长度相等且每个索引的字符都想等时,JavaScript才认为它们相等。

对象和原始值不同,首先,它们是可变的——它们的值是可修改的。对象的比较并非值的比较:即使两个对象包含相同的属性及相同的值,它们也是不相等的。各个索引元素完全相等的两个数组也不想等。

我们通常将对象成为引用类型(reference type),以此赖和JavaScript的基本类型区分开来。依照术语的叫法,对象值都是引用(reference),对象的比较均是引用的比较:当且仅当它们引用同一个基对象时,它们才相等。


八、显示类型转换

做显式类型转换最简单的方法就是使用Boolean()Number()String()Object()函数,当不实用new运算符调用这些函数时,它们会作为类型转换函数做类型转换

1
2
3
4
Number('3');                            // 3
String(false); // 'false'
Boolean([]); // true
Object(3); // new Number(3)

JavaScript中的某些运算符会做隐式的类型转换,有时用于类型转换。如果“+”运算符的一个操作数是字符串,它将会把另外一个操作数转换为字符串。一元“+”运算符将其操作数转换为数字,同样,一元“!”运算符将其操作数转换为布尔值并取反。

1
2
3
x + '';                                 // 等价于String(x)
+x; // 等价于Number(x),也可以是x-0
!!x; // 等价于Boolean(x)

九、变量声明

在JavaScript程序中,使用一个变量之前应当先声明。变量是使用关键字var来声明的

1
2
3
4
var i;
var j, sum;
var message = 'hello';
var a = 0, b = 0, c = 0;

如果未在var声明语句中给变量指定初始值,那么虽然声明了这个变量,但在给它存入一个值之前,它的初始值就是undefined

1. 变量作用域

在函数体内,局部变量的优先级高于同名的全局变量。如果在函数内声明的一个局部变量或者函数参数中带有的变量和全局变量重名,那么全局变量就会被局部变量所遮盖。

1
2
3
4
5
6
var scope = 'global';
function checkScope() {
var scope = 'local';
return scope;
}
checkScope(); // 'local'

2. 函数作用域和声明提前

在JavaScript中没有块级作用域(ES5及以前),JavaScript取而代之地使用了函数作用域(function scope):变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。

在如下所示代码中,在不同位置定义了变量i、j和k,它们都在同一个作用域内——这三个变量在函数体内均是有定义的。

1
2
3
4
5
6
7
8
9
10
11
function test(o) {
var i = 0;
if (typeof o == 'object') {
var j = 0;
for (var k = 0; k < 10; k++) {
console.log(k); // 输出数字0~9
}
console.log(k); // k已经定义了,输出10
}
console.log(j); // j已经定义了,但可能没有初始化
}

3. 作为属性的变量

当声明一个JavaScript全局变量时,实际上是定义了全局对象的一个属性。当使用var声明一个变量时,创建的属性是不可配置的,也就是说这个变量无法通过delete运算符删除。

JavaScript全局变量是全局对象的属性,这是在ES规范中强制规定的。对于局部变量则没有如此规定,但我们可以想象得到,局部变量当作跟函数调用相关的某个对象的属性。ES5规范称为“声明上下文对象”。JavaScript可以允许使用this关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象。这种存放局部变量的对象的特有性质,是一种对我们不可见的内部实现。

4. 作用域链

如果将一个局部变量看作是自定义实现的对象的属性的话,那么可以换个角度来解读变量作用域。每一段JavaScript代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。当JavaScript需要查找变量x的值的时候(这个过程称作“变量解析”(variable resolution)),它会从链中的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为x的属性,JavaScript会继续查找链上的下一个对象。如果第二个对象依然没有名为x的属性,则会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象函数属性x,那么就认为这段代码的作用链上不存在x,并最终抛出一个引用错误(ReferenceError)异常。

在JavaScript的最顶层代码中,作用域链有一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。理解对象链的创建规则是非常重要的。当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是相同的,而且关联这段代码的作用域链也不相同。


参考文献

  1. 《JavaScript权威指南(第6版)》