二、数据类型
1,数据类型概述
JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有六种。 (ES6 又新增了第七种 Symbol 类型的值,这里不涉及。)
数值(number):整数和小数(比如 1 和 3.14 )
字符串(string):文本(比如 Hello World )。
布尔值(boolean):表示真伪的两个特殊值,即 true (真)和 false (假)
undefined :表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值 null :表示空值,即此处的值为空。
对象(object):各种值组成的集合。
通常,数值、字符串、布尔值这三种类型,合称为原始类型(primitive type)的值,即它们是最基 本的数据类型,不能再细分了。对象则称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于 undefined 和 null ,一般将它 们看成两个特殊值。 对象是最复杂的数据类型,又可以分成三个子类型。
狭义的对象(object)
数组(array)
函数(function)
狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本教程的“对象”都特指狭义的对象。函 数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了 很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础。
typeof 运算符
JavaScript 有三种方法,可以确定一个值到底是什么类型。
typeof 运算符
instanceof 运算符
Object.prototype.toString 方法
instanceof 运算符和 Object.prototype.toString 方法,将在后文介绍。这里介 绍 typeof 运算符。
typeof 运算符可以返回一个值的数据类型
数值、字符串、布尔值分别返回 number 、 string 、 boolean 。
1. typeof 123 // "number"
2. typeof '123' // "string"
3. typeof false // "boolean"
函数返回 function 。
1. function f() {}
2. typeof f
3. // "function"
undefined 返回 undefined 。
1. typeof undefined
2. // "undefined"
利用这一点, typeof 可以用来检查一个没有声明的变量,而不报错。
1. v
2. // ReferenceError: v is not defined
3.
4. typeof v
5. // "undefined"
上面代码中,变量 v 没有用 var 命令声明,直接使用就会报错。但是,放在 typeof 后面,就不报错了,而是返回 undefined 。
实际编程中,这个特点通常用在判断语句。
1. // 错误的写法
2. if (v) {
3. // ...
4. }
5. // ReferenceError: v is not defined
6.
7. // 正确的写法
8. if (typeof v === "undefined") {
9. // ...
10. }
对象返回 object 。
1. typeof window // "object"
2. typeof {} // "object"
3. typeof [] // "object"
上面代码中,空数组( [] )的类型也是 object ,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。instanceof 运算符可以区分数组和对象。
1. var o = {};
2. var a = [];
3.
4. o instanceof Array // false
5. a instanceof Array // true
null 返回 object 。
1. typeof null // "object"
null 的类型是 object ,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑 null ,只把它当作 object 的一种特殊值。后来 null 独立出来,作为一种单独的数据类型,为了兼容以前的代 码, typeof null 返回 object 就没法改变了。
2,null, undefined 和布尔值
null 和 undefined
null 与 undefined 都可以表示“没有”,含义非常相似。将一个变量赋值 为 undefined 或 null ,语法效果几乎没区别。
1. var a = undefined;
2. // 或者
3. var a = null;
上面代码中,变量 a 分别被赋值为 undefined 和 null ,这两种写法的效果几乎等价。 在 if 语句中,它们都会被自动转为 false ,相等运算符( == )甚至直接报告两者相等。
1. if (!undefined) {
2. console.log('undefined is false');
3. }
4. // undefined is false
5.
6. if (!null) {
7. console.log('null is false');
8. }
9. // null is false
10.
11. undefined == null
12. // true
从上面代码可见,两者的行为是何等相似!谷歌公司开发的 JavaScript 语言的替代品 Dart 语 言,就明确规定只有 null ,没有 undefined ,既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰 吗?这与历史原因有关。 1995年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 表示”无”。根据 C 语言的传 统, null 可以自动转为 0 。
1. Number(null) // 0
2. 5 + null // 5
上面代码中, null 转为数字时,自动变成0。
但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面, null 就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不 是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果 null 自 动转为0,很不容易发现错误。 因此,他又设计了一个 undefined 。区别是这样的: null 是一个表示“空”的对象,转为数值时 为 0 ; undefined 是一个表示”此处无定义”的原始值,转为数值时为 NaN 。
1. Number(undefined) // NaN
2. 5 + undefined // NaN
对于 null 和 undefined ,大致可以像下面这样理解。 null 表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传 入 null ,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出 错,那么这个参数就会传入 null ,表示未发生错误。 undefined 表示“未定义”,下面是返回 undefined 的典型场景。
1. // 变量声明了,但没有赋值
2. var i;
3. i // undefined
4.
5. // 调用函数时,应该提供的参数没有提供,该参数等于 undefined
6. function f(x) {
7. return x;
8. }
9. f() // undefined
10.
11. // 对象没有赋值的属性
12. var o = new Object();
13. o.p // undefined
14.
15. // 函数没有返回值时,默认返回 undefined
16. function f() {}
17. f() // undefined
布尔值
布尔值代表“真”和“假”两个状态。“真”用关键字 true 表示,“假”用关键字 false 表示。布尔值 只有这两个值。 下列运算符会返回布尔值:
前置逻辑运算符: ! (Not)
相等运算符: === , !== , == , !=
比较运算符: > , >= , < , <=
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是 除了下面六个值被转为 false ,其他值都视为 true 。
undefined
null
false
0
NaN
"" 或 '' (空字符串)
布尔值往往用于程序流程的控制。
1. if ('') {
2. console.log('true');
3. }
4. // 没有任何输出
上面代码中, if 命令后面的判断条件,预期应该是一个布尔值,所以 JavaScript 自动将空字符 串,转为布尔值 false ,导致程序不会进入代码块,所以没有任何输出。 注意,空数组( [] )和空对象( {} )对应的布尔值,都是 true 。
1. if ([]) {
2. console.log('true');
3. }
4. // true
5.
6. if ({}) {
7. console.log('true');
8. }
9. // true
3,数值
整数和浮点数
JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以, 1 与 1.0 是相同的,是同一个数。
1. 1 === 1.0 // true
这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)。容易造成混 淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然 后再进行运算, 由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
1. 0.1 + 0.2 === 0.3
2. // false
3.
4. 0.3 / 0.1
5. // 2.9999999999999996
6.
7. (0.3 - 0.2) === (0.2 - 0.1)
8. // false
数值精度
根据国际标准 IEEE 754,JavaScript 浮点数的64个二进制位,从最左边开始,是这样组成的。
第1位:符号位, 0 表示正数, 1 表示负数
第2位到第12位(共11位):指数部分
第13位到第64位(共52位):小数部分(即有效数字)
符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。 指数部分一共有11个二进制位,因此大小范围就是0到2047。IEEE 754 规定,如果指数部分的值在0 到2047之间(不含两个端点),那么有效数字的第一位默认总是1,不保存在64位浮点数之中。也就是 说,有效数字这时总是 1.xx...xx 的形式,其中 xx..xx 的部分保存在64位浮点数之中,最长可 能为52位。因此,JavaScript 提供的有效数字最长为53个二进制位。
1. (-1)^符号位 * 1.xx...xx * 2^指数部分
上面公式是正常情况下(指数部分在0到2047之间),一个数在 JavaScript 内部实际的表示形式。 精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-2 53到2 53,都可以精确表示。
1. Math.pow(2, 53)
2. // 9007199254740992
3.
4. Math.pow(2, 53) + 1
5. // 9007199254740992
6.
7. Math.pow(2, 53) + 2
8. // 9007199254740994
9.
10. Math.pow(2, 53) + 3
11. // 9007199254740996
12.
13. Math.pow(2, 53) + 4
14. // 9007199254740996
上面代码中,大于2的53次方以后,整数运算的结果开始出现错误。所以,大于2的53次方的数值,都 无法保持精度。由于2的53次方是一个16位的十进制数值,所以简单的法则就是,JavaScript 对15 位的十进制数都可精确处理。
1. Math.pow(2, 53)
2. // 9007199254740992
3.
4. // 多出的三个有效数字,将无法保存
5. 9007199254740992111
6. // 9007199254740992000
上面示例表明,大于2的53次方以后,多出来的有效数字(最后三位的 111 )都会无法保存,变成 0。
数值范围
根据标准,64位浮点数的指数部分的长度是11个二进制位,意味着指数部分的最大值是2047(2的11 次方减1)。也就是说,64位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为2 1024到2 -1023(开区间),超出这个范围的数无法表示。
如果一个数大于等于2的1024次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的 数,这时就会返回 Infinity 。
1. Math.pow(2, 1024) // Infinity
如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位),那么就会发 生为“负向溢出”,即 JavaScript 无法表示这么小的数,这时会直接返回0。
1. Math.pow(2, -1075) // 0
例如:
1. var x = 0.5;
2.
3. for(var i = 0; i < 25; i++) {
4. x = x * x;
5. }
6.
7. x // 0
上面代码中,对 0.5 连续做25次平方,由于最后结果太接近0,超出了可表示的范围,JavaScript 就直接将其转为0。 JavaScript 提供 Number 对象的 MAX_VALUE 和 MIN_VALUE 属性,返回可以表示的具体的最大值和最小值。
1. Number.MAX_VALUE // 1.7976931348623157e+308
2. Number.MIN_VALUE // 5e-324
数值的表示法
JavaScript 的数值有多种表示方法,可以用字面形式直接表示,比如 35 (十进制) 和 0xFF (十六进制)。 数值也可以采用科学计数法表示,下面是几个科学计数法的例子。
1. 123e3 // 123000
2. 123e-3 // 0.123
3. -3.1E+12
4. .1e-23
科学计数法允许字母 e 或 E 的后面,跟着一个整数,表示这个数值的指数部分。
以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表 示。
(1)小数点前的数字多于21位。
1. 1234567890123456789012
2. // 1.2345678901234568e+21
3.
4. 123456789012345678901
5. // 123456789012345680000
(2)小数点后的零多于5个。
1. // 小数点后紧跟5个以上的零,
2. // 就自动转为科学计数法
3. 0.0000003 // 3e-7
4.
5. // 否则,就保持原来的字面形式
6. 0.000003 // 0.000003
数值的进制
使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进 制、十六进制、八进制、二进制。
十进制:没有前导0的数值。
八进制:有前缀 0o 或 0O 的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。
十六进制:有前缀 0x 或 0X 的数值。
二进制:有前缀 0b 或 0B 的数值。
默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制。下面是一些例子。
1. 0xff // 255
2. 0o377 // 255
3. 0b11 // 3
如果八进制、十六进制、二进制的数值里面,出现不属于该进制的数字,就会报错。
1. 0xzz // 报错
2. 0o88 // 报错
3. 0b22 // 报错
通常来说,有前导0的数值会被视为八进制,但是如果前导0后面有数字 8 和 9 ,则该数值被视为 十进制。
1. 0888 // 888
2. 0777 // 511
前导0表示八进制,处理时很容易造成混乱。ES5 的严格模式和 ES6,已经废除了这种表示法,但是浏 览器为了兼容以前的代码,目前还继续支持这种表示法。
JavaScript 提供了几个特殊的数值。
正零和负零
JavaScript 的64位浮点数之中,有一个二进制位是符号位。这意味着,任何一个数都有 一个对应的负值,就连 0 也不例外。 JavaScript 内部实际上存在2个 0 :一个是 +0 ,一个是 -0 ,区别就是64位浮点数表示法的 符号位不同。它们是等价的。
1. -0 === +0 // true
2. 0 === -0 // true
3. 0 === +0 // true
几乎所有场合,正零和负零都会被当作正常的 0 。
1. +0 // 0
2. -0 // 0
3. (-0).toString() // '0'
4. (+0).toString() // '0'
唯一有区别的场合是, +0 或 -0 当作分母,返回的值是不相等的。
1. (1 / +0) === (1 / -0) // false
上面的代码之所以出现这样结果,是因为除以正零得到 +Infinity ,除以负零得到 -Infinity , 这两者是不相等的。
NaN
(1)含义
NaN 是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成 数字出错的场合。
1. 5 - 'x' // NaN
上面代码运行时,会自动将字符串 x 转为数值,但是由于 x 不是数值,所以最后得到结果 为 NaN ,表示它是“非数字”( NaN )。 另外,一些数学函数的运算结果会出现 NaN 。
1. Math.acos(2) // NaN
2. Math.log(-1) // NaN
3. Math.sqrt(-1) // NaN
0 除以 0 也会得到 NaN 。
1. 0 / 0 // NaN
需要注意的是, NaN 不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于 Number , 使用 typeof 运算符可以看得很清楚。
1. typeof NaN // 'number'
(2)运算规则
NaN 不等于任何值,包括它本身。
1. NaN === NaN // false
数组的 indexOf 方法内部使用的是严格相等运算符,所以该方法对 NaN 不成立。
1. [NaN].indexOf(NaN) // -1
NaN 在布尔运算时被当作 false 。
1. Boolean(NaN) // false
NaN 与任何数(包括它自己)的运算,得到的都是 NaN 。
1. NaN + 32 // NaN
2. NaN - 32 // NaN
3. NaN * 32 // NaN
4. NaN / 32 // NaN
Infinity
(1)含义
Infinity 表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到 Infinity 。
1. // 场景一
2. Math.pow(2, 1024)
3. // Infinity
4.
5. // 场景二
6. 0 / 0 // NaN
7. 1 / 0 // Infinity
上面代码中,第一个场景是一个表达式的计算结果太大,超出了能够表示的范围,因此返回 Infinity 。第二个场景是 0 除以 0 会得到 NaN ,而非0数值除以 0 ,会返回 Infinity 。
Infinity 有正负之分, Infinity 表示正的无穷, -Infinity 表示负的无穷
1. Infinity === -Infinity // false
2.
3. 1 / -0 // -Infinity
4. -1 / -0 // Infinity
由于数值正向溢出(overflow)、负向溢出(underflow)和被 0 除,JavaScript 都不报错, 所以单纯的数学运算几乎没有可能抛出错误。
Infinity 大于一切数值(除了 NaN ), -Infinity 小于一切数值(除了 NaN )。
1. Infinity > 1000 // true
2. -Infinity < -1000 // true
Infinity 与 NaN 比较,总是返回 false 。
1. Infinity > NaN // false
2. -Infinity > NaN // false
3.
4. Infinity < NaN // false
5. -Infinity < NaN // false
(2)运算规则
Infinity 的四则运算,符合无穷的数学计算规则。
1. 5 * Infinity // Infinity
2. 5 - Infinity // -Infinity
3. Infinity / 5 // Infinity
4. 5 / Infinity // 0
0乘以 Infinity ,返回 NaN ;0除以 Infinity ,返回 0 ; Infinity 除以0,返回 Infinity 。
1. 0 * Infinity // NaN
2. 0 / Infinity // 0
3. Infinity / 0 // Infinity
Infinity 加上或乘以 Infinity ,返回的还是 Infinity 。
1. Infinity + Infinity // Infinity
2. Infinity * Infinity // Infinity
Infinity 减去或除以 Infinity ,得到 NaN 。
1. Infinity - Infinity // NaN
2. Infinity / Infinity // NaN
Infinity 与 null 计算时, null 会转成0,等同于与 0 的计算。
1. null * Infinity // NaN
2. null / Infinity // 0
3. Infinity / null // Infinity
Infinity 与 undefined 计算,返回的都是 NaN 。
1. undefined + Infinity // NaN
2. undefined - Infinity // NaN
3. undefined * Infinity // NaN
4. undefined / Infinity // NaN
5. Infinity / undefined // NaN
与数值相关的全局方法
parseInt()
(1)基本用法
parseInt 方法用于将字符串转为整数。
1. parseInt('123') // 123
如果字符串头部有空格,空格会被自动去除。
1. parseInt(' 81') // 81
如果 parseInt 的参数不是字符串,则会先转为字符串再转换。
1. parseInt(1.23) // 1
2. // 等同于
3. parseInt('1.23') // 1
字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返 回已经转好的部分。
1. parseInt('8a') // 8
2. parseInt('12**') // 12
3. parseInt('12.34') // 12
4. parseInt('15e2') // 15
5. parseInt('15px') // 15
上面代码中, parseInt 的参数都是字符串,结果只返回字符串头部可以转为数字的部分。
如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回 NaN 。
1. parseInt('abc') // NaN
2. parseInt('.3') // NaN
3. parseInt('') // NaN
4. parseInt('+') // NaN
5. parseInt('+1') // 1
所以, parseInt 的返回值只有两种可能,要么是一个十进制整数,要么是 NaN 。 如果字符串以 0x 或 0X 开头, parseInt 会将其按照十六进制数解析。
1. parseInt('0x10') // 16
如果字符串以 0 开头,将其按照10进制解析。
1. parseInt('011') // 11
对于那些会自动转为科学计数法的数字, parseInt 会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果。
1. parseInt(1000000000000000000000.5) // 1
2. // 等同于
3. parseInt('1e+21') // 1
4.
5. parseInt(0.0000008) // 8
6. // 等同于
7. parseInt('8e-7') // 8
(2)进制转换
parseInt 方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十 进制数。默认情况下, parseInt 的第二个参数为10,即默认是十进制转十进制。
1. parseInt('1000') // 1000
2. // 等同于
3. parseInt('1000', 10) // 1000
下面是转换指定进制的数的例子。
1. parseInt('1000', 2) // 8
2. parseInt('1000', 6) // 216
3. parseInt('1000', 8) // 512
上面代码中,二进制、六进制、八进制的 1000 ,分别等于十进制的8、216和512。这意味着,可以 用 parseInt 方法进行进制的转换。 如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结 果,超出这个范围,则返回 NaN 。如果第二个参数是 0 、 undefined 和 null ,则直接忽略。
1. parseInt('10', 37) // NaN
2. parseInt('10', 1) // NaN
3. parseInt('10', 0) // 10
4. parseInt('10', null) // 10
5. parseInt('10', undefined) // 10
如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无 法转换,则直接返回 NaN 。
1. parseInt('1546', 2) // 1
2. parseInt('546', 2) // NaN
上面代码中,对于二进制来说, 1 是有意义的字符, 5 、 4 、 6 都是无意义的字符,所以第 一行返回1,第二行返回 NaN 。 如果 parseInt 的第一个参数不是字符串,会被先转为字符串。
1. parseInt(0x11, 36) // 43
2. parseInt(0x11, 2) // 1
3.
4. // 等同于
5. parseInt(String(0x11), 36)
6. parseInt(String(0x11), 2)
7.
8. // 等同于
9. parseInt('17', 36)
10. parseInt('17', 2)
上面代码中,十六进制的 0x11 会被先转为十进制的17,再转为字符串。然后,再用36进制或二进制 解读字符串17 ,最后返回结果43和 1 。
1. parseInt(011, 2) // NaN
2.
3. // 等同于
4. parseInt(String(011), 2)
5.
6. // 等同于
7. parseInt(String(9), 2)
上面代码中,第一行的 011 会被先转为字符串 9 ,因为 9 不是二进制的有效字符,所以返 回 NaN 。如果直接计算 parseInt('011', 2) , 011 则是会被当作二进制处理,返回3。 JavaScript 不再允许将带有前缀0的数字视为八进制数,而是要求忽略这个 0 。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定。
parseFloat()
parseFloat 方法用于将一个字符串转为浮点数。
1. parseFloat('3.14') // 3.14
如果字符串符合科学计数法,则会进行相应的转换。
1. parseFloat('314e-2') // 3.14
2. parseFloat('0.0314E+2') // 3.14
如果字符串包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分。
1. parseFloat('3.14more non-digit characters') // 3.14
parseFloat 方法会自动过滤字符串前导的空格。
1. parseFloat('\t\v\r12.34\n ') // 12.34
如果参数不是字符串,或者字符串的第一个字符不能转化为浮点数,则返回 NaN 。
1. parseFloat([]) // NaN
2. parseFloat('FF2') // NaN
3. parseFloat('') // NaN
上面代码中,尤其值得注意, parseFloat 会将空字符串转为 NaN 。 这些特点使得 parseFloat 的转换结果不同于 Number 函数。
1. parseFloat(true) // NaN
2. Number(true) // 1
3.
4. parseFloat(null) // NaN
5. Number(null) // 0
6.
7. parseFloat('') // NaN
8. Number('') // 0
9.
10. parseFloat('123.45#') // 123.45
11. Number('123.45#') // NaN
isNaN()
isNaN 方法可以用来判断一个值是否为 NaN 。
1. isNaN(NaN) // true
2. isNaN(123) // false
但是, isNaN 只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串 会被先转成 NaN ,所以最后返回 true ,这一点要特别引起注意。 isNaN 为 true 的值,有可能不是 NaN ,而是一个字符串。
1. isNaN('Hello') // true
2. // 相当于
3. isNaN(Number('Hello')) // true
出于同样的原因,对于对象和数组, isNaN 也返回 true 。
1. isNaN({}) // true
2. // 等同于
3. isNaN(Number({})) // true
4.
5. isNaN(['xzy']) // true
6. // 等同于
7. isNaN(Number(['xzy'])) // true
但是,对于空数组和只有一个数值成员的数组, isNaN 返回 false 。
1. isNaN([]) // false
2. isNaN([123]) // false
3. isNaN(['123']) // false
上面代码之所以返回 false ,原因是这些数组能被 Number 函数转成数值,因此,使用 isNaN 之前,最好判断一下数据类型。
1. function myIsNaN(value) {
2. return typeof value === 'number' && isNaN(value);
3. }
判断 NaN 更可靠的方法是,利用 NaN 为唯一不等于自身的值的这个特点,进行判断。
1. function myIsNaN(value) {
2. return value !== value;
3. }
isFinite()
isFinite 方法返回一个布尔值,表示某个值是否为正常的数值。
1. isFinite(Infinity) // false
2. isFinite(-Infinity) // false
3. isFinite(NaN) // false
4. isFinite(undefined) // false
5. isFinite(null) // true
6. isFinite(-1) // true
除了 Infinity 、 -Infinity 、 NaN 和 undefined 这几个值会返 回 false , isFinite 对于其他的数值都会返回 true 。
4,字符串
字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。
1. 'abc'
2. "abc"
单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号。
1. 'key = "value"'
2. "It's a long journey"
上面两个都是合法的字符串。 如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引 号字符串内部使用双引号,也是如此。
1. 'Did she say \'Hello\'?'
2. // "Did she say 'Hello'?"
3.
4. "Did she say \"Hello\"?"
5. // "Did she say "Hello"?"
由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引 号。 字符串默认只能写在一行内,分成多行将会报错。
1. 'a
2. b
3. c'
4. // SyntaxError: Unexpected token ILLEGAL
上面代码将一个字符串分成三行,JavaScript 就会报错。
如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。
1. var longString = 'Long \
2. long \
3. long \
4. string';
5.
6. longString
7. // "Long long long string"
上面代码表示,加了反斜杠以后,原来写在一行的字符串,可以分成多行书写。但是,输出的时候还是 单行,效果与写在同一行完全一样。注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空 格),否则会报错。
连接运算符( + )可以连接多个单行字符串,将长字符串拆成多行书写,输出的时候也是单行。
1. var longString = 'Long '
2. + 'long '
3. + 'long '
4. + 'string';
如果想输出多行字符串,有一种利用多行注释的变通方法。
1. (function () { /*
2. line 1
3. line 2
4. line 3
5. */}).toString().split('\n').slice(1, -1).join('\n')
6. // "line 1
7. // line 2
8. // line 3"
上面的例子中,输出的字符串就是多行。
转义
反斜杠(\)在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。 需要用反斜杠转义的特殊字符,主要有下面这些。
\0 :null( \u0000 )
\b :后退键( \u0008 )
\f :换页符( \u000C )
\n :换行符( \u000A )
\r :回车键( \u000D )
\t :制表符( \u0009 )
\v :垂直制表符( \u000B )
\' :单引号( \u0027 )
\" :双引号( \u0022 )
\\ :反斜杠( \u005C )
上面这些字符前面加上反斜杠,都表示特殊含义。
1. console.log('1\n2')
2. // 1
3. // 2
上面代码中, \n 表示换行,输出的时候就分成了两行。
反斜杠还有三种特殊用法。
(1) \HHH 反斜杠后面紧跟三个八进制数( 000 到 377 ),代表一个字符。 HHH 对应该字符的 Unicode 码点,比如 \251 表示版权符号。显然,这种方法只能输出256种字符。
(2) \xHH \x 后面紧跟两个十六进制数( 00 到 FF ),代表一个字符。 HH 对应该字符的 Unicode 码点,比如 \xA9 表示版权符号。这种方法也只能输出256种字符。
(3) \uXXXX \u 后面紧跟四个十六进制数( 0000 到 FFFF ),代表一个字符。 XXXX 对应该字符的 Unicode 码点,比如 \u00A9 表示版权符号。 下面是这三种字符特殊写法的例子。
1. '\251' // "©"
2. '\xA9' // "©"
3. '\u00A9' // "©"
4.
5. '\172' === 'z' // true
6. '\x7A' === 'z' // true
7. '\u007A' === 'z' // true
如果在非特殊字符前面使用反斜杠,则反斜杠会被省略。
1. '\a'
2. // "a"
上面代码中, a 是一个正常字符,前面加反斜杠没有特殊含义,反斜杠会被自动省略。 如果字符串的正常内容之中,需要包含反斜杠,则反斜杠前面需要再加一个反斜杠,用来对自身转义。
1. "Prev \\ Next"
2. // "Prev \ Next"
字符串与数组
字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号 从0开始)。
1. var s = 'hello';
2. s[0] // "h"
3. s[1] // "e"
4. s[4] // "o"
5.
6. // 直接对字符串使用方括号运算符
7. 'hello'[1] // "e"
如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回 undefined 。
1. 'abc'[3] // undefined
2. 'abc'[-1] // undefined
3. 'abc'['x'] // undefined
但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。
1. var s = 'hello';
2.
3. delete s[0];
4. s // "hello"
5.
6. s[1] = 'a';
7. s // "hello"
8.
9. s[5] = '!';
10. s // "hello"
上面代码表示,字符串内部的单个字符无法改变和增删,这些操作会失败。
length 属性
length 属性返回字符串的长度,该属性也是无法改变的。
1. var s = 'hello';
2. s.length // 5
3.
4. s.length = 3;
5. s.length // 5
6.
7. s.length = 7;
8. s.length // 5
上面代码表示字符串的 length 属性无法改变,但是不会报错。
字符集
JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。 JavaScript 不仅以 Unicode 储存字符,还允许直接在程序中使用 Unicode 码点表示字符,即将 字符写成 \uxxxx 的形式,其中 xxxx 代表该字符的 Unicode 码点。比如, \u00A9 代表版权 符号。
1. var s = '\u00A9';
2. s // "©"
解析代码的时候,JavaScript 会自动识别一个字符是字面形式表示,还是 Unicode 形式表示。输 出给用户的时候,所有字符都会转成字面形式。
1. var f\u006F\u006F = 'abc';
2. foo // "abc"
上面代码中,第一行的变量名 foo 是 Unicode 形式表示,第二行是字面形式表示。JavaScript 会自动识别。 每个字符在 JavaScript 内部都是以16位(即2个字节)的 UTF-16 格式储存。JavaScript 的单位字符长度固定为16位长度,即2个字节。
UTF-16 有两种长度:对于码点在 U+0000 到 U+FFFF 之间的字符,长度为16位(即2个字 节);对于码点在 U+10000 到 U+10FFFF 之间的字符,长度为32位(即4个字节),而且前两个字 节在 0xD800 到 0xDBFF 之间,后两个字节在 0xDC00 到 0xDFFF 之间。举例来说,码 点 U+1D306 对应的字符为 亖, 它写成 UTF-16 就是 0xD834 0xDF06 。
JavaScript 对 UTF-16 的支持是不完整的,由于历史原因,只支持两字节的字符,不支持四字节的 字符。这是因为 JavaScript 第一版发布的时候,Unicode 的码点只编到 U+FFFF ,因此两字节 足够表示了。后来,Unicode 纳入的字符越来越多,出现了四字节的编码。但是,JavaScript 的标 准此时已经定型了,统一将字符长度限制在两字节,导致无法识别四字节的字符。上一节的那个四字节 字符 亖 ,浏览器会正确识别这是一个字符,但是 JavaScript 无法识别,会认为这是两个字符。
1. '亖'.length // 2
上面代码中,JavaScript 认为 ꊬ 的长度为2,而不是1。 对于码点在 U+10000 到 U+10FFFF 之间的字符,JavaScript 总是认为它们是两个字 符( length 属性为2)。所以处理的时候,必须把这一点考虑在内,JavaScript 返回 的字符串长度可能是不正确的。
Base64 转码
文本里面包含一些不可打印的符号,比如 ASCII 码0到31的符号都无法打印出来,这时可以使 用 Base64 编码,将它们转成可以打印的字符。另一个场景是,有时需要以文本格式传递二进制数 据,那么也可以使用 Base64 编码。
Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、 + 和 / 这64个字符组 成的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。
JavaScript 原生提供两个 Base64 相关的方法。
btoa() :任意值转为 Base64 编码
atob() :Base64 编码转为原来的值
1. var string = 'Hello World!';
2. btoa(string) // "SGVsbG8gV29ybGQh"
3. atob('SGVsbG8gV29ybGQh') // "Hello World!"
注意,这两个方法不适合非 ASCII 码的字符,会报错。
1. btoa('你好') // 报错
要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。
1. function b64Encode(str) {
2. return btoa(encodeURIComponent(str));
3. }
4.
5. function b64Decode(str) {
6. return decodeURIComponent(atob(str));
7. }
8.
9. b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
10. b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
5,对象
对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。 什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。
1. var obj = {
2. foo: 'Hello',
3. bar: 'World'
4. };
上面代码中,大括号就定义了一个对象,它被赋值给变量 obj ,所以变量 obj 就指向一个对象。 该对象内部包含两个键值对(又称为两个“成员”),第一个键值对是 foo: 'Hello' ,其 中 foo 是“键名”(成员的名称),字符串 Hello 是“键值”(成员的值)。键名与键值之间用冒号 分隔。第二个键值对是 bar: 'World' , bar 是键名, World 是键值。两个键值对之间用逗号 分隔。
键名
对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以。 上面的代码也可以写成下面这样。
1. var obj = {
2. 'foo': 'Hello',
3. 'bar': 'World'
4. };
如果键名是数值,会被自动转为字符串。
1. var obj = {
2. 1: 'a',
3. 3.2: 'b',
4. 1e2: true,
5. 1e-2: true,
6. .234: true,
7. 0xFF: true
8. };
9.
10. obj
11. // Object {
12. // 1: "a",
13. // 3.2: "b",
14. // 100: true,
15. // 0.01: true,
16. // 0.234: true,
17. // 255: true
18. // }
19.
20. obj['100'] // true
上面代码中,对象 obj 的所有键名都被自动转成了字符串。
如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则 必须加上引号,否则会报错。
1. // 报错
2. var obj = {
3. 1p: 'Hello World'
4. };
5.
6. // 不报错
7. var obj = {
8. '1p': 'Hello World',
9. 'h w': 'Hello World',
10. 'p+q': 'Hello World'
11. };
上面对象的三个键名,都不符合标识名的条件,所以必须加上引号。
对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值 为函数,通常把这个属性称为“方法”,它可以像函数那样调用。
1. var obj = {
2. p: function (x) {
3. return 2 * x;
4. }
5. };
6.
7. obj.p(1) // 2
上面代码中,对象 obj 的属性 p ,就指向一个函数。
如果属性的值还是一个对象,就形成了链式引用。
1. var o1 = {};
2. var o2 = { bar: 'hello' };
3.
4. o1.foo = o2;
5. o1.foo.bar // "hello"
上面代码中,对象 o1 的属性 foo 指向对象 o2 ,就可以链式引用 o2 的属性。
对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。
1. var obj = {
2. p: 123,
3. m: function () { ... },
4. }
属性可以动态创建,不必在对象声明时就指定。
1. var obj = {};
2. obj.foo = 123;
3. obj.foo // 123
对象的引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,指向同一个内存地址。修 改其中一个变量,会影响到其他所有变量。
1. var o1 = {};
2. var o2 = o1;
3.
4. o1.a = 1;
5. o2.a // 1
6.
7. o2.b = 2;
8. o1.b // 2
上面代码中, o1 和 o2 指向同一个对象,因此为其中任何一个变量添加属性,另一个变量都可以 读写该属性。如果取消某一个变量对于原对象的引用,不会影响到另一个变量。
1. var o1 = {};
2. var o2 = o1;
3.
4. o1 = 1;
5. o2 // {}
上面代码中, o1 和 o2 指向同一个对象,然后 o1 的值变为1,这时不会对 o2 产生影 响, o2 还是指向原来的那个对象。 但是,这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷 贝。
1. var x = 1;
2. var y = x;
3.
4. x = 2;
5. y // 1
上面的代码中,当 x 的值发生变化后, y 的值并不变,这就表示 y 和 x 并不是指向同一个内 存地址。
对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?
1. { foo: 123 }
JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表 示一个包含 foo 属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标 签 foo ,指向表达式 123 。 为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块, 一律解释为代码块。
1. { console.log(123) } // 123
上面的语句是一个代码块,而且只有解释为代码块,才能执行。 如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号 只能解释为对象。
1. ({ foo: 123 }) // 正确
2. ({ console.log(123) }) // 报错
这种差异在 eval 语句(作用是对字符串求值)中反映得最明显。
1. eval('{foo: 123}') // 123
2. eval('({foo: 123})') // {foo: 123}
上面代码中,如果没有圆括号, eval 将其理解为一个代码块;加上圆括号以后,就理解成一个对 象。
属性的读取操作
读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。
1. var obj = {
2. p: 'Hello World'
3. };
4.
5. obj.p // "Hello World"
6. obj['p'] // "Hello World"
上面代码分别采用点运算符和方括号运算符,读取属性 p 。
请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。
1. var foo = 'bar';
2.
3. var obj = {
4. foo: 1,
5. bar: 2
6. };
7.
8. obj.foo // 1
9. obj[foo] // 2
上面代码中,引用对象 obj 的 foo 属性时,如果使用点运算符, foo 就是字符串;如果使用方 括号运算符,但是不使用引号,那么 foo 就是一个变量,指向字符串 bar 。
方括号运算符内部还可以使用表达式。
1. obj['hello' + ' world']
2. obj[3 + 3]
数字键可以不加引号,因为会自动转成字符串。
1. var obj = {
2. 0.7: 'Hello World'
3. };
4.
5. obj['0.7'] // "Hello World"
6. obj[0.7] // "Hello World"
上面代码中,对象 obj 的数字键 0.7 ,加不加引号都可以,因为会被自动转为字符串。 注意,数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。
1. var obj = {
2. 123: 'hello world'
3. };
4.
5. obj.123 // 报错
6. obj[123] // "hello world"
上面代码的第一个表达式,对数值键名 123 使用点运算符,结果报错。第二个表达式使用方括号运算 符,结果就是正确的。
属性的赋值操作
点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。
1. var obj = {};
2.
3. obj.foo = 'Hello';
4. obj['bar'] = 'World';
上面代码中,分别使用点运算符和方括号运算符,对属性赋值。 JavaScript 允许属性的“后绑定”,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。
1. var obj = { p: 1 };
2.
3. // 等价于
4.
5. var obj = {};
6. obj.p = 1;
属性的查看
查看一个对象本身的所有属性,可以使用 Object.keys 方法。
1. var obj = {
2. key1: 1,
3. key2: 2
4. };
5.
6. Object.keys(obj);
7. // ['key1', 'key2']
属性的删除:delete 命令
delete 命令用于删除对象的属性,删除成功后返回 true 。
1. var obj = { p: 1 };
2. Object.keys(obj) // ["p"]
3.
4. delete obj.p // true
5. obj.p // undefined
6. Object.keys(obj) // []
上面代码中, delete 命令删除对象 obj 的 p 属性。删除后,再读取 p 属性就会返 回 undefined ,而且 Object.keys 方法的返回值也不再包括该属性。 注意,删除一个不存在的属性, delete 不报错,而且返回 true 。
1. var obj = {};
2. delete obj.p // true
上面代码中,对象 obj 并没有 p 属性,但是 delete 命令照样返回 true 。因此,不能根 据 delete 命令的结果,认定某个属性是存在的。 只有一种情况, delete 命令会返回 false ,那就是该属性存在,且不得删除。
1. var obj = Object.defineProperty({}, 'p', {
2. value: 123,
3. configurable: false
4. });
5.
6. obj.p // 123
7. delete obj.p // false
上面代码之中,对象 obj 的 p 属性是不能删除的,所以 delete 命令返回 false (关 于 Object.defineProperty 方法的介绍,请看《标准库》的 Object 对象一章)。 delete 命令只能删除对象本身的属性,无法删除继承的属性。
1. var obj = {};
2. delete obj.toString // true
3. obj.toString // function toString() { [native code] }
上面代码中, toString 是对象 obj 继承的属性,虽然 delete 命令返回 true ,但该属性并 没有被删除,依然存在。这个例子还说明,即使 delete 返回 true ,该属性依然可能读取到值。
属性是否存在:in 运算符
in 运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返 回 true ,否则返回 false 。它的左边是一个字符串,表示属性名,右边是一个对象。
1. var obj = { p: 1 };
2. 'p' in obj // true
3. 'toString' in obj // true
in 运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码 中,对象 obj 本身并没有 toString 属性,但是 in 运算符会返回 true ,因为这个属性是继 承的。
这时,可以使用对象的 hasOwnProperty 方法判断一下,是否为对象自身的属性。
1. var obj = {};
2. if ('toString' in obj) {
3. console.log(obj.hasOwnProperty('toString')) // false
4. }
属性的遍历:for…in 循环
for...in 循环用来遍历一个对象的全部属性。
1. var obj = {a: 1, b: 2, c: 3};
2.
3. for (var i in obj) {
4. console.log('键名:', i);
5. console.log('键值:', obj[i]);
6. }
7. // 键名: a
8. // 键值: 1
9. // 键名: b
10. // 键值: 2
11. // 键名: c
12. // 键值: 3
for...in 循环有两个使用注意点。
它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
它不仅遍历对象自身的属性,还遍历继承的属性。
举例来说,对象都继承了 toString 属性,但是 for...in 循环不会遍历到这个属性。
1. var obj = {};
2.
3. // toString 属性是存在的
4. obj.toString // toString() { [native code] }
5.
6. for (var p in obj) {
7. console.log(p);
8. } // 没有任何输出
上面代码中,对象 obj 继承了 toString 属性,该属性不会被 for...in 循环遍历到,因为它默 认是“不可遍历”的。关于对象属性的可遍历性,如果继承的属性是可遍历的,那么就会被 for...in 循环遍历到。但是,一般情况下,都是只想遍历 对象自身的属性,所以使用 for...in 的时候,应该结合使用 hasOwnProperty 方法,在循环内部 判断一下,某个属性是否为对象自身的属性。
1. var person = { name: '老张' };
2.
3. for (var key in person) {
4. if (person.hasOwnProperty(key)) {
5. console.log(key);
6. }
7. }
8. // name
with 语句
with 语句的格式如下:
1. with (对象) {
2. 语句;
3. }
它的作用是操作同一个对象的多个属性时,提供一些书写的方便。
1. // 例一
2. var obj = {
3. p1: 1,
4. p2: 2,
5. };
6. with (obj) {
7. p1 = 4;
8. p2 = 5;
9. }
10. // 等同于
11. obj.p1 = 4;
12. obj.p2 = 5;
13.
14. // 例二
15. with (document.links[0]){
16. console.log(href);
17. console.log(title);
18. console.log(style);
19. }
20. // 等同于
21. console.log(document.links[0].href);
22. console.log(document.links[0].title);
23. console.log(document.links[0].style);
注意,如果 with 区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当 前作用域的全局变量。
1. var obj = {};
2. with (obj) {
3. p1 = 4;
4. p2 = 5;
5. }
6.
7. obj.p1 // undefined
8. p1 // 4
上面代码中,对象 obj 并没有 p1 属性,对 p1 赋值等于创造了一个全局变量 p1 。正确的写 法应该是,先定义对象 obj 的属性 p1 ,然后在 with 区块内操作它。 这是因为 with 区块没有改变作用域,它的内部依然是当前作用域。这造成了 with 语句的一个很 大的弊病,就是绑定对象不明确。
1. with (obj) {
2. console.log(x);
3. }
单纯从上面的代码块,根本无法判断 x 到底是全局变量,还是对象 obj 的一个属性。这非常不利 于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速 度。因此,建议不要使用 with 语句,可以考虑用一个临时变量代替 with 。
1. with(obj1.obj2.obj3) {
2. console.log(p1 + p2);
3. }
4.
5. // 可以写成
6. var temp = obj1.obj2.obj3;
7. console.log(temp.p1 + temp.p2);
6,函数
函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。
函数的声明
(1)function 命令
function 命令声明的代码区块,就是一个函数。 function 命令后面是函数名,函数名后面是一 对圆括号,里面是传入函数的参数。函数体放在大括号里面。
1. function print(s) {
2. console.log(s);
3. }
上面的代码命名了一个 print 函数,以后使用 print() 这种形式,就可以调用相应的代码。这叫 做函数的声明(Function Declaration)。
(2)函数表达式
除了用 function 命令声明函数,还可以采用变量赋值的写法。
1. var print = function(s) {
2. console.log(s);
3. };
这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。 采用函数表达式声明函数时, function 命令后面不带有函数名。如果加上函数名,该函数名只在函 数体内部有效,在函数体外部无效。
1. var print = function x(){
2. console.log(typeof x);
3. };
4.
5. x
6. // ReferenceError: x is not defined
7.
8. print()
9. // function
上面代码在函数表达式中,加入了函数名 x 。这个 x 只在函数体内部可用,指代函数表达式本 身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除 错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明 函数也非常常见。
1. var f = function f() {};
需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括 号后面不用加分号。这两种声明函数的方式,差别很细微,可以近似认为是等价的。
(3)Function 构造函数
第三种声明函数的方式是 Function 构造函数。
1. var add = new Function(
2. 'x',
3. 'y',
4. 'return x + y'
5. );
6.
7. // 等同于
8. function add(x, y) {
9. return x + y;
10. }
上面代码中, Function 构造函数接受三个参数,除了最后一个参数是 add 函数的“函数体”,其 他参数都是 add 函数的参数。 你可以传递任意数量的参数给 Function 构造函数,只有最后一个参数会被当做函数体,如果只有一 个参数,该参数就是函数体。
1. var foo = new Function(
2. 'return "hello world";'
3. );
4.
5. // 等同于
6. function foo() {
7. return 'hello world';
8. }
Function 构造函数可以不使用 new 命令,返回结果完全一样。这种声明函数的方式非常不直观,几乎无人使用。
函数的重复声明
如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。
1. function f() {
2. console.log(1);
3. }
4. f() // 2
5.
6. function f() {
7. console.log(2);
8. }
9. f() // 2
上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升,前一次声明 在任何时候都是无效的,这一点要特别注意。
圆括号运算符,return 语句和递归
调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。
1. function add(x, y) {
2. return x + y;
3. }
4.
5. add(1, 1) // 2
上面代码中,函数名后面紧跟一对圆括号,就会调用这个函数。 函数体内部的 return 语句,表示返回。JavaScript 引擎遇到 return 语句,就直接返 回 return 后面的那个表达式的值,后面即使还有语句,也不会得到执行。 return 语 句所带的那个表达式,就是函数的返回值。 return 语句不是必需的,如果没有的话,该函数就不返 回任何值,或者说返回 undefined 。 函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码。
1. function fib(num) {
2. if (num === 0) return 0;
3. if (num === 1) return 1;
4. return fib(num - 2) + fib(num - 1);
5. }
6.
7. fib(6) // 8
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以 使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他 函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。 由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。
1. function add(x, y) {
2. return x + y;
3. }
4.
5. // 将函数赋值给一个变量
6. var operator = add;
7.
8. // 将函数作为参数和返回值
9. function a(op){
10. return op;
11. }
12. a(add)(1, 1)
13. // 2
函数名的提升
JavaScript 引擎将函数名视同变量名,所以采用 function 命令声明函数时,整个函数会像变量 声明一样,被提升到代码头部。所以,下面的代码不会报错。
1. f();
2.
3. function f() {}
表面上,上面代码好像在声明之前就调用了函数 f 。但是实际上,由于“变量提升”,函数 f 被提 升到了代码头部,在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错。
1. f();
2. var f = function (){};
3. // TypeError: undefined is not a function
上面的代码等同于下面的形式。
1. var f;
2. f();
3. f = function () {};
上面代码第二行,调用 f 的时候, f 只是被声明了,还没有被赋值,等于 undefined ,所以会 报错。 注意,如果像下面例子那样,采用 function 命令和 var 赋值语句声明同一个函数,由于存在函数 提升,最后会采用 var 赋值语句的定义。
1. var f = function () {
2. console.log('1');
3. }
4.
5. function f() {
6. console.log('2');
7. }
8.
9. f() // 1
上面例子中,表面上后面声明的函数 f ,应该覆盖前面的 var 赋值语句,但是由于存在函数提 升,实际上正好反过来。
函数的属性和方法:name 属性
函数的 name 属性返回函数的名字。
1. function f1() {}
2. f1.name // "f1"
如果是通过变量赋值定义的函数,那么 name 属性返回变量名。
1. var f2 = function () {};
2. f2.name // "f2"
但是,上面这种情况,只有在变量的值是一个匿名函数时才是如此。如果变量的值是一个具名函数,那 么 name 属性返回 function 关键字之后的那个函数名。
1. var f3 = function myName() {};
2. f3.name // 'myName'
上面代码中, f3.name 返回函数表达式的名字。注意,真正的函数名还是 f3 ,而 myName 这个 名字只在函数体内部可用。
name 属性的一个用处,就是获取参数函数的名字。
1. var myFunc = function () {};
2.
3. function test(f) {
4. console.log(f.name);
5. }
6.
7. test(myFunc) // myFunc
上面代码中,函数 test 内部通过 name 属性,就可以知道传入的参数是什么函数。
length 属性
函数的 length 属性返回函数预期传入的参数个数,即函数定义之中的参数个数。
1. function f(a, b) {}
2. f.length // 2
上面代码定义了空函数 f ,它的 length 属性就是定义时的参数个数。不管调用时输入了多少个参 数, length 属性始终等于2。
length 属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重 载”(overload)。
toString()
函数的 toString() 方法返回一个字符串,内容是函数的源码。
1. function f() {
2. a();
3. b();
4. c();
5. }
6.
7. f.toString()
8. // function f() {
9. // a();
10. // b();
11. // c();
12. // }
对于那些原生的函数, toString() 方法返回 function (){[native code]} 。
1. Math.sqrt.toString()
2. // "function sqrt() { [native code] }"
上面代码中, Math.sqrt() 是 JavaScript 引擎提供的原生函数, toString() 方法就返回原 生代码的提示。
函数内部的注释也可以返回。
1. function f() {/*
2. 这是一个
3. 多行注释
4. */}
5.
6. f.toString()
7. // "function f(){/*
8. // 这是一个
9. // 多行注释
10. // */}"
利用这一点,可以变相实现多行字符串。
1. var multiline = function (fn) {
2. var arr = fn.toString().split('\n');
3. return arr.slice(1, arr.length - 1).join('\n');
4. };
5.
6. function f() {/*
7. 这是一个
8. 多行注释
9. */}
10.
11. multiline(f);
12. // " 这是一个
13. // 多行注释"
函数作用域
作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种 是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函 数内部存在。
对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读 取。
1. var v = 1;
2.
3. function f() {
4. console.log(v);
5. }
6.
7. f()
8. // 1
上面的代码表明,函数 f 内部可以读取全局变量 v 。 在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)。
1. function f(){
2. var v = 1;
3. }
4.
5. v // ReferenceError: v is not defined
上面代码中,变量 v 在函数内部定义,所以是一个局部变量,函数之外就无法读取。函数内部定义的变量,会在该作用域内覆盖同名全局变量。
1. var v = 1;
2.
3. function f(){
4. var v = 2;
5. console.log(v);
6. }
7.
8. f() // 2
9. v // 1
上面代码中,变量 v 同时在函数的外部和内部有定义。在函数内部定义,局部变量 v 覆盖 了全局变量 v 。对于 var 命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。
1. if (true) {
2. var x = 5;
3. }
4. console.log(x); // 5
上面代码中,变量 x 在条件判断区块之中声明,结果就是一个全局变量,可以在区块之外读取。
函数内部的变量提升
与全局作用域一样,函数作用域内部也会产生“变量提升”现象。 var 命令声明的变量,不管在什么 位置,变量声明都会被提升到函数体的头部。
1. function foo(x) {
2. if (x > 100) {
3. var tmp = x - 100;
4. }
5. }
6.
7. // 等同于
8. function foo(x) {
9. var tmp;
10. if (x > 100) {
11. tmp = x - 100;
12. };
13. }
函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其 运行时所在的作用域无关。
1. var a = 1;
2. var x = function () {
3. console.log(a);
4. };
5.
6. function f() {
7. var a = 2;
8. x();
9. }
10.
11. f() // 1
上面代码中,函数 x 是在函数 f 的外部声明的,所以它的作用域绑定外层,内部变量 a 不会到 函数 f 体内取值,所以输出 1 ,而不是 2 。 总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。 如果函数 A 调用函数 B ,却没考虑到函数 B 不会引用函数 A 的内部变 量。
1. var x = function () {
2. console.log(a);
3. };
4.
5. function y(f) {
6. var a = 2;
7. f();
8. }
9.
10. y(x)
11. // ReferenceError: a is not defined
上面代码将函数 x 作为参数,传入函数 y 。但是,函数 x 是在函数 y 体外声明的,作用域绑 定外层,因此找不到函数 y 的内部变量 a ,导致报错。 同样的,函数体内部声明的函数,作用域绑定函数体内部。
1. function foo() {
2. var x = 1;
3. function bar() {
4. console.log(x);
5. }
6. return bar;
7. }
8.
9. var x = 2;
10. var f = foo();
11. f() // 1
上面代码中,函数 foo 内部声明了一个函数 bar , bar 的作用域绑定 foo 。当我们 在 foo 外部取出 bar 执行时,变量 x 指向的是 foo 内部的 x ,而不是 foo 外部 的 x 。正是这种机制,构成了“闭包”现象。
参数
函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参 数。
1. function square(x) {
2. return x * x;
3. }
4.
5. square(2) // 4
6. square(3) // 9
参数的省略
函数参数不是必需的,JavaScript 允许省略参数。
1. function f(a, b) {
2. return a;
3. }
4.
5. f(1, 2, 3) // 1
6. f(1) // 1
7. f() // undefined
8.
9. f.length // 2
上面代码的函数 f 定义了两个参数,但是运行时无论提供多少个参数(或者不提供参数), JavaScript 都不会报错。省略的参数的值就变为 undefined 。需要注意的是,函数 的 length 属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。 但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传 入 undefined 。
1. function f(a, b) {
2. return a;
3. }
4.
5. f( , 1) // SyntaxError: Unexpected token ,(…)
6. f(undefined, 1) // undefined
上面代码中,如果省略第一个参数,就会报错。
传递方式
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。在函数体内修改参数值,不会影响到函数外部。
1. var p = 2;
2.
3. function f(p) {
4. p = 3;
5. }
6. f(p);
7.
8. p // 2
上面代码中,变量 p 是一个原始类型的值,传入函数 f 的方式是传值传递。因此,在函数内 部, p 的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。 但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
1. var obj = { p: 1 };
2.
3. function f(o) {
4. o.p = 2;
5. }
6. f(obj);
7.
8. obj.p // 2
上面代码中,传入函数 f 的是参数对象 obj 的地址。因此,在函数内部修改 obj 的属性 p , 会影响到原始值。 注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始 值
1. var obj = [1, 2, 3];
2.
3. function f(o) {
4. o = [2, 3, 4];
5. }
6. f(obj);
7.
8. obj // [1, 2, 3]
上面代码中,在函数 f() 内部,参数对象 obj 被整个替换成另一个值。这时不会影响到原始值。 这是因为,形式参数( o )的值实际是参数 obj 的地址,重新对 o 赋值导致 o 指向另一个地 址,保存在原地址上的值当然不受影响。
同名参数
如果有同名的参数,则取最后出现的那个值。
1. function f(a, a) {
2. console.log(a);
3. }
4.
5. f(1, 2) // 2
上面代码中,函数 f() 有两个参数,且参数名都是 a 。取值的时候,以后面的 a 为准,即使后 面的 a 没有值或被省略,也是以其为准。
1. function f(a, a) {
2. console.log(a);
3. }
4.
5. f(1) // undefined
调用函数 f() 的时候,没有提供第二个参数, a 的取值就变成了 undefined 。这时,如果要获 得第一个 a 的值,可以使用 arguments 对象。
1. function f(a, a) {
2. console.log(arguments[0]);
3. }
4.
5. f(1) // 1
arguments 对象
(1)定义 由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参 数。这就是 arguments 对象的由来。 arguments 对象包含了函数运行时的所有参数, arguments[0] 就是第一个参 数, arguments[1] 就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
1. var f = function (one) {
2. console.log(arguments[0]);
3. console.log(arguments[1]);
4. console.log(arguments[2]);
5. }
6.
7. f(1, 2, 3)
8. // 1
9. // 2
10. // 3
正常模式下, arguments 对象可以在运行时修改
1. var f = function(a, b) {
2. arguments[0] = 3;
3. arguments[1] = 2;
4. return a + b;
5. }
6.
7. f(1, 1) // 5
上面代码中,函数 f() 调用时传入的参数,在函数内部被修改成 3 和 2 。 严格模式下, arguments 对象与函数参数不具有联动关系。修改 arguments 对象不会 影响到实际的函数参数。
1. var f = function(a, b) {
2. 'use strict'; // 开启严格模式
3. arguments[0] = 3;
4. arguments[1] = 2;
5. return a + b;
6. }
7.
8. f(1, 1) // 2
上面代码中,函数体内是严格模式,这时修改 arguments 对象,不会影响到真实参数 a 和 b 。 通过 arguments 对象的 length 属性,可以判断函数调用时到底带几个参数。
1. function f() {
2. return arguments.length;
3. }
4.
5. f(1, 2, 3) // 3
6. f(1) // 1
7. f() // 0
(2)与数组的关系
需要注意的是,虽然 arguments 很像数组,但它是一个对象。数组专有的方法(比 如 slice 和 forEach ),不能在 arguments 对象上直接使用。 如果要让 arguments 对象使用数组方法,真正的解决方法是将 arguments 转为真正的数组。下面 是两种常用的转换方法: slice 方法和逐一填入新数组。
1. var args = Array.prototype.slice.call(arguments);
2.
3. // 或者
4. var args = [];
5. for (var i = 0; i < arguments.length; i++) {
6. args.push(arguments[i]);
7. }
(3)callee 属性
arguments 对象带有一个 callee 属性,返回它所对应的原函数
1. var f = function () {
2. console.log(arguments.callee === f);
3. }
4.
5. f() // true
可以通过 arguments.callee ,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此 不建议使用。
闭包
闭包(closure)是 JavaScript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实 现。 理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作 用域。函数内部可以直接读取全局变量。
1. var n = 999;
2.
3. function f1() {
4. console.log(n);
5. }
6. f1() // 999
上面代码中,函数 f1 可以读取全局变量 n 。 但是,函数外部无法读取函数内部声明的变量。
1. function f1() {
2. var n = 999;
3. }
4.
5. console.log(n)
6. // Uncaught ReferenceError: n is not defined(
上面代码中,函数 f1 内部声明的变量 n ,函数外是无法读取的。 如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能 实现。那就是在函数的内部,再定义一个函数。
1. function f1() {
2. var n = 999;
3. function f2() {
4. console.log(n); // 999
5. }
6. }
上面代码中,函数 f2 就在函数 f1 内部,这时 f1 内部的所有局部变量,对 f2 都是可见的。 但是反过来就不行, f2 内部的局部变量,对 f1 就是不可见的。这就是 JavaScript 语言特有 的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父 对象的所有变量,对子对象都是可见的,反之则不成立。 既然 f2 可以读取 f1 的局部变量,那么只要把 f2 作为返回值,就可以在 f1 外部读取 它的内部变量了。
1. function f1() {
2. var n = 999;
3. function f2() {
4. console.log(n);
5. }
6. return f2;
7. }
8.
9. var result = f1();
10. result(); // 999
上面代码中,函数 f1 的返回值就是函数 f2 ,由于 f2 可以读取 f1 的内部变量,所以就可以 在外部获得 f1 的内部变量了。 闭包就是函数 f2 ,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数 内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大 的特点,就是它可以“记住”诞生的环境,比如 f2 记住了它诞生的环境 f1 ,所以从 f2 可以得 到 f1 的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。 闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中, 即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结 果。
1. function createIncrementor(start) {
2. return function () {
3. return start++;
4. };
5. }
6.
7. var inc = createIncrementor(5);
8.
9. inc() // 5
10. inc() // 6
11. inc() // 7
上面代码中, start 是函数 createIncrementor 的内部变量。通过闭包, start 的状态被保 留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包 inc 使得函 数 createIncrementor 的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接 口。
为什么会这样呢?原因就在于 inc 始终在内存中,而 inc 的存在依赖于 createIncrementor , 因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。 闭包的另一个用处,是封装对象的私有属性和私有方法。
1. function Person(name) {
2. var _age;
3. function setAge(n) {
4. _age = n;
5. }
6. function getAge() {
7. return _age;
8. }
9.
10. return {
11. name: name,
12. getAge: getAge,
13. setAge: setAge
14. };
15. }
16.
17. var p1 = Person('张三');
18. p1.setAge(25);
19. p1.getAge() // 25
上面代码中,函数 Person 的内部变量 _age ,通过闭包 getAge 和 setAge ,变成了返回对 象 p1 的私有变量。 注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内 存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
立即调用的函数表达式(IIFE)
在 JavaScript 中,圆括号 () 是一种运算符,跟在函数名之后,表示调用该函数。比 如, print() 就表示调用 print 函数。 有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会 产生语法错误。
1. function(){ /* code */ }();
2. // SyntaxError: Unexpected token (
产生这个错误的原因是, function 这个关键字即可以当作语句,也可以当作表达式。
1. // 语句
2. function f() {}
3.
4. // 表达式
5. var f = function f() {}
为了避免解析上的歧义,JavaScript 引擎规定,如果 function 关键字出现在行首,一律解释成 语句。因此,JavaScript 引擎看到行首是 function 关键字之后,认为这一段都是函数的定义, 不应该以圆括号结尾,所以就报错了。 解决方法就是不要让 function 出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将 其放在一个圆括号里面。
1. (function(){ /* code */ }());
2. // 或者
3. (function(){ /* code */ })();
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就 避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。 注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。
1. // 报错
2. (function(){ /* code */ }())
3. (function(){ /* code */ }())
上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参 数。 任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。
1. var i = function(){ return 10; }();
2. true && function(){ /* code */ }();
3. 0, function(){ /* code */ }();
甚至像下面这样写,也是可以的。
1. !function () { /* code */ }();
2. ~function () { /* code */ }();
3. -function () { /* code */ }();
4. +function () { /* code */ }();
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命 名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的 私有变量。
1. // 写法一
2. var tmp = newData;
3. processData(tmp);
4. storeData(tmp);
5.
6. // 写法二
7. (function () {
8. var tmp = newData;
9. processData(tmp);
10. storeData(tmp);
11. }());
上面代码中,写法二比写法一更好,因为完全避免了污染全局变量
eval 命令
eval 命令接受一个字符串作为参数,并将这个字符串当作语句执行。
1. eval('var a = 1;');
2. a // 1
上面代码将字符串当作语句运行,生成了变量 a 。 如果参数字符串无法当作语句运行,那么就会报错。
1. eval('3x') // Uncaught SyntaxError: Invalid or unexpected token
放在 eval 中的字符串,应该有独自存在的意义,不能用来与 eval 以外的命令配合使用。例如:
1. eval('return;'); // Uncaught SyntaxError: Illegal return statement
上面代码会报错,因为 return 不能单独使用,必须在函数中使用。 如果 eval 的参数不是字符串,那么会原样返回。
1. eval(123) // 123
eval 没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安 全问题。
1. var a = 1;
2. eval('a = 2');
3.
4. a // 2
上面代码中, eval 命令修改了外部变量 a 的值。由于这个原因, eval 有安全风险。 为了防止这种风险,JavaScript 规定,如果使用严格模式, eval 内部声明的变量,不会影响到 外部作用域。
1. (function f() {
2. 'use strict';
3. eval('var foo = 123');
4. console.log(foo); // ReferenceError: foo is not defined
5. })()
上面代码中,函数 f 内部是严格模式,这时 eval 内部声明的 foo 变量,就不会影响到外部。 不过,即使在严格模式下, eval 依然可以读写当前作用域的变量。
1. (function f() {
2. 'use strict';
3. var foo = 1;
4. eval('foo = 2');
5. console.log(foo); // 2
6. })()
上面代码中,严格模式下, eval 内部还是改写了外部变量,可见安全风险依然存在。 总之, eval 的本质是在当前作用域之中,注入代码。由于安全风险和不利于 JavaScript 引擎优 化执行速度,所以一般不推荐使用。通常情况下, eval 最常见的场合是解析 JSON 数据的字符 串,不过正确的做法应该是使用原生的 JSON.parse 方法。
eval 的别名调用
前面说过 eval 不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的 阶段,根本无法分辨执行的是 eval 。
1. var m = eval;
2. m('var x = 1');
3. x // 1
上面代码中,变量 m 是 eval 的别名。静态代码分析阶段,引擎分辨不出 m('var x = 1') 执行 的是 eval 命令。 为了保证 eval 的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执 行 eval , eval 内部一律是全局作用域。
1. var a = 1;
2.
3. function f() {
4. var a = 2;
5. var e = eval;
6. e('console.log(a)');
7. }
8.
9. f() // 1
上面代码中, eval 是别名调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出 的 a 为全局变量。这样的话,引擎就能确认 e() 不会对当前的函数作用域产生影响,优化的时候 就可以把这一行排除掉。 eval 的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分 辨 eval() 这一种形式是直接调用。
1. eval.call(null, '...')
2. window.eval('...')
3. (1, eval)('...')
4. (eval, eval)('...')
上面这些形式都是 eval 的别名调用,作用域都是全局作用域。
7,数组
数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表 示。
1. var arr = ['a', 'b', 'c'];
上面代码中的 a 、 b 、 c 就构成一个数组,两端的方括号是数组的标志。 a 是0号位 置, b 是1号位置, c 是2号位置。 除了在定义时赋值,数组也可以先定义后赋值。
1. var arr = [];
2.
3. arr[0] = 'a';
4. arr[1] = 'b';
5. arr[2] = 'c';
任何类型的数据,都可以放入数组。
1. var arr = [
2. {a: 1},
3. [1, 2, 3],
4. function() {return true;}
5. ];
6.
7. arr[0] // Object {a: 1}
8. arr[1] // [1, 2, 3]
9. arr[2] // function (){return true;}
上面数组 arr 的3个成员依次是对象、数组、函数。 如果数组的元素还是数组,就形成了多维数组。
1. var a = [[1, 2], [3, 4]];
2. a[0][1] // 2
3. a[1][1] // 4
数组的本质
本质上,数组属于一种特殊的对象。 typeof 运算符会返回数组的类型是 object 。
1. typeof [1, 2, 3] // "object"
上面代码表明, typeof 运算符认为数组的类型就是对象。 数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2…)。
1. var arr = ['a', 'b', 'c'];
2.
3. Object.keys(arr)
4. // ["0", "1", "2"]
上面代码中, Object.keys 方法返回数组的所有键名。可以看到数组的键名就是整数0、1、2。 由于数组成员的键名是固定的(默认总是0、1、2…),因此数组不用为每个元素指定键名,而对象的每 个成员都必须指定键名。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实 也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。
1. var arr = ['a', 'b', 'c'];
2.
3. arr['0'] // 'a'
4. arr[0] // 'a'
上面代码分别用数值和字符串作为键名,结果都能读取数组。原因是数值键名被自动转为了字符串。 注意,这点在赋值时也成立。一个值总是先转成字符串,再作为键名进行赋值。
1. var a = [];
2.
3. a[1.00] = 6;
4. a[1] // 6
上面代码中,由于 1.00 转成字符串是 1 ,所以通过数字键 1 可以读取值。 上一章说过,对象有两种读取成员的方法:点结构( object.key )和方括号结构 ( object[key] )。但是,对于数值的键名,不能使用点结构。
1. var arr = [1, 2, 3];
2. arr.0 // SyntaxError
上面代码中, arr.0 的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组 成员只能用方括号 arr[0] 表示(方括号是运算符,可以接受数值)。
length 属性
数组的 length 属性,返回数组的成员数量。
1. ['a', 'b', 'c'].length // 3
JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(2 ³² - 1)个,也就是说 length 属性的最大值就是 4294967295。 只要是数组,就一定有 length 属性。该属性是一个动态的值,等于键名中的最大整数加上 1 。
1. var arr = ['a', 'b'];
2. arr.length // 2
3.
4. arr[2] = 'c';
5. arr.length // 3
6.
7. arr[9] = 'd';
8. arr.length // 10
9.
10. arr[1000] = 'e';
11. arr.length // 1001
上面代码表示,数组的数字键不需要连续, length 属性的值总是比最大的那个整数键大 1 。另 外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。 length 属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少 到 length 设置的值。
1. var arr = [ 'a', 'b', 'c' ];
2. arr.length // 3
3.
4. arr.length = 2;
5. arr // ["a", "b"]
上面代码表示,当数组的 length 属性设为2(即最大的整数键只能是1)那么整数键2(值为 c ) 就已经不在数组中了,被自动删除了。
清空数组的一个有效方法,就是将 length 属性设为0。
1. var arr = [ 'a', 'b', 'c' ];
2.
3. arr.length = 0;
4. arr // []
如果人为设置 length 大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空 位。
1. var a = ['a'];
2.
3. a.length = 3;
4. a[1] // undefined
上面代码表示,当 length 属性设为大于数组个数时,读取新增的位置都会返回 undefined 。 如果人为设置 length 为不合法的值,JavaScript 会报错。
1. // 设置负值
2. [].length = -1
3. // RangeError: Invalid array length
4.
5. // 数组元素个数大于等于2的32次方
6. [].length = Math.pow(2, 32)
7. // RangeError: Invalid array length
8.
9. // 设置字符串
10. [].length = 'abc'
11. // RangeError: Invalid array length
值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响 length 属性 的值。
1. var a = [];
2.
3. a['p'] = 'abc';
4. a.length // 0
5.
6. a[2.1] = 'abc';
7. a.length // 0
上面代码将数组的键分别设为字符串和小数,结果都不影响 length 属性。因为, length 属性的 值就是等于最大的数字键加1,而这个数组没有整数键,所以 length 属性保持为 0 。 如果数组的键名是添加超出范围的数值,该键名会自动转为字符串。
1. var arr = [];
2. arr[-1] = 'a';
3. arr[Math.pow(2, 32)] = 'b';
4.
5. arr.length // 0
6. arr[-1] // "a"
7. arr[4294967296] // "b"
上面代码中,我们为数组 arr 添加了两个不合法的数字键,结果 length 属性没有发生变化。这些 数字键都变成了字符串键名。最后两行之所以会取到值,是因为取键值时,数字键名会默认转为字符 串。
in 运算符
检查某个键名是否存在的运算符 in ,适用于对象,也适用于数组。
1. var arr = [ 'a', 'b', 'c' ];
2. 2 in arr // true
3. '2' in arr // true
4. 4 in arr // false
上面代码表明,数组存在键名为 2 的键。由于键名都是字符串,所以数值 2 会自动转成字符串。 注意,如果数组的某个位置是空位, in 运算符返回 false 。
1. var arr = [];
2. arr[100] = 'a';
3.
4. 100 in arr // true
5. 1 in arr // false
上面代码中,数组 arr 只有一个成员 arr[100] ,其他位置的键名都会返回 false 。
for…in 循环和数组的遍历
for...in 循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象。
1. var a = [1, 2, 3];
2.
3. for (var i in a) {
4. console.log(a[i]);
5. }
6. // 1
7. // 2
8. // 3
但是, for...in 不仅会遍历数组所有的数字键,还会遍历非数字键。
1. var a = [1, 2, 3];
2. a.foo = true;
3.
4. for (var key in a) {
5. console.log(key);
6. }
7. // 0
8. // 1
9. // 2
10. // foo
上面代码在遍历数组时,也遍历到了非整数键 foo 。所以,不推荐使用 for...in 遍历数组。 数组的遍历可以考虑使用 for 循环或 while 循环。
1. var a = [1, 2, 3];
2.
3. // for循环
4. for(var i = 0; i < a.length; i++) {
5. console.log(a[i]);
6. }
7.
8. // while循环
9. var i = 0;
10. while (i < a.length) {
11. console.log(a[i]);
12. i++;
13. }
14.
15. var l = a.length;
16. while (l--) {
17. console.log(a[l]);
18. }
上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。 数组的 forEach 方法,也可以用来遍历数组。
1. var colors = ['red', 'green', 'blue'];
2. colors.forEach(function (color) {
3. console.log(color);
4. });
5. // red
6. // green
7. // blue
数组的空位
当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。
1. var a = [1, , 1];
2. a.length // 3
上面代码表明,数组的空位不影响 length 属性。 需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都 是一样的。
1. var a = [1, 2, 3,];
2.
3. a.length // 3
4. a // [1, 2, 3]
上面代码中,数组最后一个成员后面有一个逗号,这不影响 length 属性的值,与没有这个逗号时效 果一样。 数组的空位是可以读取的,返回 undefined 。
1. var a = [, , ,];
2. a[1] // undefined
使用 delete 命令删除一个数组成员,会形成空位,并且不会影响 length 属性。
1. var a = [1, 2, 3];
2. delete a[1];
3.
4. a[1] // undefined
5. a.length // 3
上面代码用 delete 命令删除了数组的第二个元素,这个位置就形成了空位,但是对 length 属性 没有影响。 length 属性不过滤空位。所以,使用 length 属性进行数组遍历,一定要 非常小心。 数组的某个位置是空位,与某个位置是 undefined ,是不一样的。如果是空位,使用数组 的 forEach 方法、 for...in 结构、以及 Object.keys 方法进行遍历,空位都会被跳过。
1. var a = [, , ,];
2.
3. a.forEach(function (x, i) {
4. console.log(i + '. ' + x);
5. })
6. // 不产生任何输出
7.
8. for (var i in a) {
9. console.log(i);
10. }
11. // 不产生任何输出
12.
13. Object.keys(a)
14. // []
如果某个位置是 undefined ,遍历的时候就不会被跳过。
1. var a = [undefined, undefined, undefined];
2.
3. a.forEach(function (x, i) {
4. console.log(i + '. ' + x);
5. });
6. // 0. undefined
7. // 1. undefined
8. // 2. undefined
9.
10. for (var i in a) {
11. console.log(i);
12. }
13. // 0
14. // 1
15. // 2
16.
17. Object.keys(a)
18. // ['0', '1', '2']
空位就是数组没有这个元素,所以不会被遍历到,而 undefined 则表示数组有这个元 素,值是 undefined ,所以遍历不会跳过。
类似数组的对象
如果一个对象的所有键名都是正整数或零,并且有 length 属性,那么这个对象就很像数组,语法上 称为“类似数组的对象”(array-like object)。
1. var obj = {
2. 0: 'a',
3. 1: 'b',
4. 2: 'c',
5. length: 3
6. };
7.
8. obj[0] // 'a'
9. obj[1] // 'b'
10. obj.length // 3
11. obj.push('d') // TypeError: obj.push is not a function
上面代码中,对象 obj 就是一个类似数组的对象。但是,“类似数组的对象”并不是数组,因为它们 不具备数组特有的方法。对象 obj 没有数组的 push 方法,使用该方法就会报错。 “类似数组的对象”的根本特征,就是具有 length 属性。只要有 length 属性,就可以认为这个对 象类似于数组。但是有一个问题,这种 length 属性不是动态值,不会随着成员的变化而变化。
1. var obj = {
2. length: 0
3. };
4. obj[3] = 'd';
5. obj.length // 0
上面代码为对象 obj 添加了一个数字键,但是 length 属性没变。这就说明了 obj 不是数组。典型的“类似数组的对象”是函数的 arguments 对象,以及大多数 DOM 元素集,还有字符串。
1. // arguments对象
2. function args() { return arguments }
3. var arrayLike = args('a', 'b');
4.
5. arrayLike[0] // 'a'
6. arrayLike.length // 2
7. arrayLike instanceof Array // false
8.
9. // DOM元素集
10. var elts = document.getElementsByTagName('h3');
11. elts.length // 3
12. elts instanceof Array // false
13.
14. // 字符串
15. 'abc'[1] // 'b'
16. 'abc'.length // 3
17. 'abc' instanceof Array // false
上面代码包含三个例子,它们都不是数组( instanceof 运算符返回 false ),但是看上去都非常 像数组。 数组的 slice 方法可以将“类似数组的对象”变成真正的数组。
1. var arr = Array.prototype.slice.call(arrayLike);
除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过 call() 把数 组的方法放到对象上面。
1. function print(value, index) {
2. console.log(index + ' : ' + value);
3. }
4.
5. Array.prototype.forEach.call(arrayLike, print);
上面代码中, arrayLike 代表一个类似数组的对象,本来是不可以使用数组的 forEach() 方法 的,但是通过 call() ,可以把 forEach() 嫁接到 arrayLike 上面调用。 下面的例子就是通过这种方法,在 arguments 对象上面调用 forEach 方法。
1. // forEach 方法
2. function logArgs() {
3. Array.prototype.forEach.call(arguments, function (elem, i) {
4. console.log(i + '. ' + elem);
5. });
6. }
7.
8. // 等同于 for 循环
9. function logArgs() {
10. for (var i = 0; i < arguments.length; i++) {
11. console.log(i + '. ' + arguments[i]);
12. }
13. }
字符串也是类似数组的对象,也可以用 Array.prototype.forEach.call 遍历。
1. Array.prototype.forEach.call('abc', function (chr) {
2. console.log(chr);
3. });
4. // a
5. // b
6. // c
注意,这种方法比直接使用数组原生的 forEach 要慢,所以最好还是先将“类似数组的对象”转为真 正的数组,然后再直接调用数组的 forEach 方法。
1. var arr = Array.prototype.slice.call('abc');
2. arr.forEach(function (chr) {
3. console.log(chr);
4. });
5. // a
6. // b
7. // c