变量
变量的声明和赋值,是分开的两个步骤,上面的代码将它们合在了一起,实际的步骤是下面这样。
var a;
a = 1;
如果只是声明变量而没有赋值,则该变量的值是undefined
。undefined
是一个 JavaScript 关键字,表示“无定义”。
var a;
a // undefined
如果变量赋值的时候,忘了写var
命令,这条语句也是有效的。
var a = 1;
// 基本等同
a = 1;
但是,不写var
的做法,不利于表达意图,而且容易不知不觉地创建全局变量,所以建议总是使用var
命令声明变量。
如果一个变量没有声明就直接使用,JavaScript 会报错,告诉你变量未定义。
x
// ReferenceError: x is not defined
上面代码直接使用变量x
,系统就报错,告诉你变量x
没有声明。
可以在同一条var
命令中声明多个变量。
var a, b;
JavaScript 是一种动态类型语言,也就是说,变量的类型没有限制,变量可以随时更改类型。
var a = 1;
a = 'hello';
上面代码中,变量a
起先被赋值为一个数值,后来又被重新赋值为一个字符串。第二次赋值的时候,因为变量a
已经存在,所以不需要使用var
命令。
如果使用var
重新声明一个已经存在的变量,是无效的。
var x = 1;
var x;
x // 1
上面代码中,变量x
声明了两次,第二次声明是无效的。
变量提升
JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。
console.log(a);
var a = 1;
上面代码首先使用console.log
方法,在控制台(console)显示变量a
的值。这时变量a
还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错。因为存在变量提升,真正运行的是下面的代码。
var a;
console.log(a);
a = 1;
最后的结果是显示undefined
,表示变量a
已声明,但还未赋值。
标识符
中文是合法的标识符,可以用作变量名。
var 临时变量 = 1;
注释
由于历史上 JavaScript 可以兼容 HTML 代码的注释,所以<!--
和-->
也被视为合法的单行注释。
x = 1; <!-- x = 2;
--> x = 3;
上面代码中,只有x = 1
会执行,其他的部分都被注释掉了。
需要注意的是,-->
只有在行首,才会被当成单行注释,否则会当作正常的运算。
function countdown(n) {
while (n --> 0) console.log(n);
}
countdown(3)
// 2
// 1
// 0
上面代码中,n --> 0
实际上会当作n-- > 0
,因此输出2、1、0。
区块
JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。
对于var
命令来说,JavaScript 的区块不构成单独的作用域(scope)。
{
var a = 1;
}
a // 1
上面代码在区块内部,使用var
命令声明并赋值了变量a
,然后在区块外部,变量a
依然有效,区块对于var
命令不构成单独的作用域,与不使用区块的情况没有任何区别。在 JavaScript 语言中,单独使用区块并不常见,区块往往用来构成其他更复杂的语法结构,比如for
、if
、while
、function
等。
条件语句
上面代码的原意是,当x
等于y
的时候,才执行相关语句。但是,不小心将严格相等运算符写成赋值表达式,结果变成了将y
赋值给变量x
,再判断变量x
的值(等于2)的布尔值(结果为true
)。
这种错误可以正常生成一个布尔值,因而不会报错。为了避免这种情况,有些开发者习惯将常量写在运算符的左边,这样的话,一旦不小心将相等运算符写成赋值运算符,就会报错,因为常量不能被赋值。
if (x = 2) { // 不报错
if (2 = x) { // 报错
else
代码块总是与离自己最近的那个if
语句配对。
var m = 1;
var n = 2;
if (m !== 1)
if (n === 2) console.log('hello');
else console.log('world');
上面代码不会有任何输出,else
代码块不会得到执行,因为它跟着的是最近的那个if
语句,相当于下面这样。
if (m !== 1) {
if (n === 2) {
console.log('hello');
} else {
console.log('world');
}
}
var x = 1;
switch (x) {
case 1:
console.log('x 等于1');
case 2:
console.log('x 等于2');
default:
console.log('x 等于其他值');
}
// x等于1
// x等于2
// x等于其他值
上面代码中,case
代码块之中没有break
语句,导致不会跳出switch
结构,而会一直执行下去。正确的写法是像下面这样。
switch (x) {
case 1:
console.log('x 等于1');
break;
case 2:
console.log('x 等于2');
break;
default:
console.log('x 等于其他值');
}
switch
语句部分和case
语句部分,都可以使用表达式。
switch(1 + 3) {
case 2 + 2:
f();
break;
default:
neverHappens();
}
需要注意的是,switch
语句后面的表达式,与case
语句后面的表示式比较运行结果时,采用的是严格相等运算符(===
),而不是相等运算符(==
),这意味着比较时不会发生类型转换。
var x = 1;
switch (x) {
case true:
console.log('x 发生类型转换');
default:
console.log('x 没有发生类型转换');
}
// x 没有发生类型转换
上面代码中,由于变量x
没有发生类型转换,所以不会执行case true
的情况。这表明,switch
语句内部采用的是“严格相等运算符”,
for
语句的三个部分(initialize、test、increment),可以省略任何一个,也可以全部省略。
for ( ; ; ){
console.log('Hello World');
}
上面代码省略了for
语句表达式的三个部分,结果就导致了一个无限循环。
标签(label)
JavaScript 语言允许,语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下。
label:
语句
标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。
标签通常与break
语句和continue
语句配合使用,跳出特定的循环。
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) break top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
上面代码为一个双重循环区块,break
命令后面加上了top
标签(注意,top
不用加引号),满足条件时,直接跳出双层循环。如果break
语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。
continue
语句也可以与标签配合使用。
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) continue top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// i=2, j=0
// i=2, j=1
// i=2, j=2
上面代码中,continue
命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮外层循环。如果continue
语句后面不使用标签,则只能进入下一轮的内层循环。
概述
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
。
typeof 123 // "number"
typeof '123' // "string"
typeof false // "boolean"
函数返回function
。
function f() {}
typeof f
// "function"
undefined
返回undefined
。
typeof undefined
// "undefined"
利用这一点,typeof
可以用来检查一个没有声明的变量,而不报错。
v
// ReferenceError: v is not defined
typeof v
// "undefined"
上面代码中,变量v
没有用var
命令声明,直接使用就会报错。但是,放在typeof
后面,就不报错了,而是返回undefined
。
实际编程中,这个特点通常用在判断语句。
// 错误的写法
if (v) {
// ...
}
// ReferenceError: v is not defined
// 正确的写法
if (typeof v === "undefined") {
// ...
}
对象返回object
。
typeof window // "object"
typeof {} // "object"
typeof [] // "object"
上面代码中,空数组([]
)的类型也是object
,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。这里顺便提一下,instanceof
运算符可以区分数组和对象。instanceof
运算符的详细解释,请见《面向对象编程》一章。
var o = {};
var a = [];
o instanceof Array // false
o instanceof Object // true
a instanceof Array // true
null
返回object
。
typeof null // "object"
typeof undefined // "undefined"
null
的类型是object
,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑null
,只把它当作object
的一种特殊值。后来null
独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null
返回object
就没法改变了。
null 和 undefined
概述
null
与undefined
都可以表示“没有”,含义非常相似。将一个变量赋值为undefined
或null
,老实说,语法效果几乎没区别。
var a = undefined;
// 或者
var a = null;
上面代码中,变量a
分别被赋值为undefined
和null
,这两种写法的效果几乎等价。
在if
语句中,它们都会被自动转为false
,相等运算符(==
)甚至直接报告两者相等。
if (!undefined) {
console.log('undefined is false');
}
// undefined is false
if (!null) {
console.log('null is false');
}
// null is false
undefined == null // true
undefined === null // false
"" == '' // true
"" === '' // true
从上面代码可见,两者的行为是何等相似!谷歌公司开发的 JavaScript 语言的替代品 Dart 语言,就明确规定只有null
,没有undefined
!
既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这与历史原因有关。
1995年 JavaScript 诞生时,最初像 Java 一样,只设置了null
表示”无”。根据 C 语言的传统,null
可以自动转为0
。
Number(null) // 0
5 + null // 5
Number("10") // 10
上面代码中,null
转为数字时,自动变成0。
但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面,null
就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果null
自动转为0,很不容易发现错误。
因此,他又设计了一个undefined
。区别是这样的:null
是一个表示“空”的对象,转为数值时为0
;undefined
是一个表示”此处无定义”的原始值,转为数值时为NaN
。
Number(undefined) // NaN
5 + undefined // NaN
用法和含义
对于null
和undefined
,大致可以像下面这样理解。
null
表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入null
,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入null
,表示未发生错误。
undefined
表示“未定义”,下面是返回undefined
的典型场景。
// 变量声明了,但没有赋值
var i;
i // undefined
// 调用函数时,应该提供的参数没有提供,该参数等于 undefined
function f(x) {
return x;
}
f() // undefined
// 对象没有赋值的属性
var o = new Object();
o.p // undefined
// 函数没有返回值时,默认返回 undefined
function f() {}
f() // undefined
布尔值
布尔值代表“真”和“假”两个状态。“真”用关键字true
表示,“假”用关键字false
表示。布尔值只有这两个值。
下列运算符会返回布尔值:
- 两元逻辑运算符:
&&
(And),||
(Or) - 前置逻辑运算符:
!
(Not) - 相等运算符:
===
,!==
,==
,!=
- 比较运算符:
>
,>=
,<
,<=
如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false
,其他值都视为true
。
undefined
null
false
0
NaN
""
或''
(空字符串)
布尔值往往用于程序流程的控制,请看一个例子。
if ('') {
console.log('true');
}
// 没有任何输出
上面代码中,if
命令后面的判断条件,预期应该是一个布尔值,所以 JavaScript 自动将空字符串,转为布尔值false
,导致程序不会进入代码块,所以没有任何输出。
注意,空数组([]
)和空对象({}
)对应的布尔值,都是true
。
if ([]) {
console.log('true');
}
// true
if ({}) {
console.log('true');
}
// true
数组
任何类型的数据,都可以放入数组。
var arr = [
{a: 1},
[1, 2, 3],
function() {return true;}
];
arr[0] // Object {a: 1}
arr[1] // [1, 2, 3]
arr[2] // function (){return true;}
如果数组的元素还是数组,就形成了多维数组。
var a = [[1, 2], [3, 4]];
a[0][1] // 2
a[1][1] // 4
本质上,数组属于一种特殊的对象。typeof
运算符会返回数组的类型是object
。
typeof [1, 2, 3] // "object"
上面代码表明,typeof
运算符认为数组的类型就是对象。
数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2…)。
var arr = ['a', 'b', 'c'];
Object.keys(arr)
// ["0", "1", "2"]
由于数组成员的键名是固定的(默认总是0、1、2…),因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。
var arr = ['a', 'b', 'c'];
arr['0'] // 'a'
arr[0] // 'a'
上面代码分别用数值和字符串作为键名,结果都能读取数组。原因是数值键名被自动转为了字符串。
注意,这点在赋值时也成立。如果一个值总是先转成字符串,再进行赋值。
var a = [];
a[1.00] = 6;
a[1] // 6
上面代码中,由于1.00
转成字符串是1
,所以通过数字键1
可以读取值。
上一章说过,对象有两种读取成员的方法:点结构(object.key
)和方括号结构(object[key]
)。但是,对于数值的键名,不能使用点结构。
var arr = [1, 2, 3];
arr.0 // SyntaxError
上面代码中,arr.0
的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号arr[0]
表示(方括号是运算符,可以接受数值)。
只要是数组,就一定有length
属性。该属性是一个动态的值,等于键名中的最大整数加上1
。
var arr = ['a', 'b'];
arr.length // 2
arr[2] = 'c';
arr.length // 3
arr[9] = 'd';
arr.length // 10
arr[1000] = 'e';
arr.length // 1001
上面代码表示,数组的数字键不需要连续,length
属性的值总是比最大的那个整数键大1
。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。
length
属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length
设置的值。
var arr = [ 'a', 'b', 'c' ];
arr.length // 3
arr.length = 2;
arr // ["a", "b"]
上面代码表示,当数组的length
属性设为2(即最大的整数键只能是1)那么整数键2(值为c
)就已经不在数组中了,被自动删除了。
清空数组的一个有效方法,就是将length
属性设为0。
var arr = [ 'a', 'b', 'c' ];
arr.length = 0;
arr // []
如果人为设置length
大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位。
var a = ['a'];
a.length = 3;
a[1] // undefined
上面代码表示,当length
属性设为大于数组个数时,读取新增的位置都会返回undefined
。
如果人为设置length
为不合法的值,JavaScript 会报错。
// 设置负值
[].length = -1
// RangeError: Invalid array length
// 数组元素个数大于等于2的32次方
[].length = Math.pow(2, 32)
// RangeError: Invalid array length
// 设置字符串
[].length = 'abc'
// RangeError: Invalid array length
值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length
属性的值。
var a = [];
a['p'] = 'abc';
a.length // 0
a[2.1] = 'abc';
a.length // 0
上面代码将数组的键分别设为字符串和小数,结果都不影响length
属性。因为,length
属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length
属性保持为0
。
如果数组的键名是添加超出范围的数值,该键名会自动转为字符串。
var arr = [];
arr[-1] = 'a';
arr[Math.pow(2, 32)] = 'b';
arr.length // 0
arr[-1] // "a"
arr[4294967296] // "b"
上面代码中,我们为数组arr
添加了两个不合法的数字键,结果length
属性没有发生变化。这些数字键都变成了字符串键名。最后两行之所以会取到值,是因为取键值时,数字键名会默认转为字符串。
in 运算符
检查某个键名是否存在的运算符in
,适用于对象,也适用于数组。
var arr = [ 'a', 'b', 'c' ];
2 in arr // true
'2' in arr // true
4 in arr // false
上面代码表明,数组存在键名为2
的键。由于键名都是字符串,所以数值2
会自动转成字符串。
注意,如果数组的某个位置是空位,in
运算符返回false
。
var arr = [];
arr[100] = 'a';
100 in arr // true
1 in arr // false
上面代码中,数组arr
只有一个成员arr[100]
,其他位置的键名都会返回false
。
for...in
不仅会遍历数组所有的数字键,还会遍历非数字键。
var a = [1, 2, 3];
a.foo = true;
for (var key in a) {
console.log(key);
}
// 0
// 1
// 2
// foo
上面代码在遍历数组时,也遍历到了非整数键foo
。所以,不推荐使用for...in
遍历数组。
数组的遍历可以考虑使用for
循环或while
循环。
var a = [1, 2, 3];
// for循环
for(var i = 0; i < a.length; i++) {
console.log(a[i]);
}
// while循环
var i = 0;
while (i < a.length) {
console.log(a[i]);
i++;
}
var l = a.length;
while (l--) {
console.log(a[l]);
}
上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。
数组的forEach
方法,也可以用来遍历数组,详见《标准库》的 Array 对象一章。
var colors = ['red', 'green', 'blue'];
colors.forEach(function (color) {
console.log(color);
});
// red
// green
// blue
数组的空位
当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。
var a = [1, , 1];
a.length // 3
上面代码表明,数组的空位不影响length
属性。
需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的。
var a = [1, 2, 3,];
a.length // 3
a // [1, 2, 3]
上面代码中,数组最后一个成员后面有一个逗号,这不影响length
属性的值,与没有这个逗号时效果一样。
数组的空位是可以读取的,返回undefined
。
var a = [, , ,];
a[1] // undefined
使用delete
命令删除一个数组成员,会形成空位,并且不会影响length
属性。
var a = [1, 2, 3];
delete a[1];
a[1] // undefined
a.length // 3
上面代码用delete
命令删除了数组的第二个元素,这个位置就形成了空位,但是对length
属性没有影响。也就是说,length
属性不过滤空位。所以,使用length
属性进行数组遍历,一定要非常小心。
数组的某个位置是空位,与某个位置是undefined
,是不一样的。如果是空位,使用数组的forEach
方法、for...in
结构、以及Object.keys
方法进行遍历,空位都会被跳过。
var a = [, , ,];
a.forEach(function (x, i) {
console.log(i + '. ' + x);
})
// 不产生任何输出
for (var i in a) {
console.log(i);
}
// 不产生任何输出
Object.keys(a)
// []
如果某个位置是undefined
,遍历的时候就不会被跳过。
var a = [undefined, undefined, undefined];
a.forEach(function (x, i) {
console.log(i + '. ' + x);
});
// 0. undefined
// 1. undefined
// 2. undefined
for (var i in a) {
console.log(i);
}
// 0
// 1
// 2
Object.keys(a)
// ['0', '1', '2']
这就是说,空位就是数组没有这个元素,所以不会被遍历到,而undefined
则表示数组有这个元素,值是undefined
,所以遍历不会跳过。
数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示。
var arr = ['a', 'b', 'c'];
上面代码中的a
、b
、c
就构成一个数组,两端的方括号是数组的标志。a
是0号位置,b
是1号位置,c
是2号位置。
除了在定义时赋值,数组也可以先定义后赋值。
var arr = [];
arr[0] = 'a';
arr[1] = 'b';
arr[2] = 'c';
任何类型的数据,都可以放入数组。
var arr = [
{a: 1},
[1, 2, 3],
function() {return true;}
];
arr[0] // Object {a: 1}
arr[1] // [1, 2, 3]
arr[2] // function (){return true;}
上面数组arr
的3个成员依次是对象、数组、函数。
如果数组的元素还是数组,就形成了多维数组。
var a = [[1, 2], [3, 4]];
a[0][1] // 2
a[1][1] // 4
数组的本质
本质上,数组属于一种特殊的对象。typeof
运算符会返回数组的类型是object
。
typeof [1, 2, 3] // "object"
上面代码表明,typeof
运算符认为数组的类型就是对象。
数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2…)。
var arr = ['a', 'b', 'c'];
Object.keys(arr)
// ["0", "1", "2"]
上面代码中,Object.keys
方法返回数组的所有键名。可以看到数组的键名就是整数0、1、2。
由于数组成员的键名是固定的(默认总是0、1、2…),因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。
var arr = ['a', 'b', 'c'];
arr['0'] // 'a'
arr[0] // 'a'
上面代码分别用数值和字符串作为键名,结果都能读取数组。原因是数值键名被自动转为了字符串。
注意,这点在赋值时也成立。如果一个值总是先转成字符串,再进行赋值。
var a = [];
a[1.00] = 6;
a[1] // 6
上面代码中,由于1.00
转成字符串是1
,所以通过数字键1
可以读取值。
上一章说过,对象有两种读取成员的方法:点结构(object.key
)和方括号结构(object[key]
)。但是,对于数值的键名,不能使用点结构。
var arr = [1, 2, 3];
arr.0 // SyntaxError
上面代码中,arr.0
的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号arr[0]
表示(方括号是运算符,可以接受数值)。
length 属性
数组的length
属性,返回数组的成员数量。
['a', 'b', 'c'].length // 3
JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个(232 - 1)个,也就是说length
属性的最大值就是 4294967295。
只要是数组,就一定有length
属性。该属性是一个动态的值,等于键名中的最大整数加上1
。
var arr = ['a', 'b'];
arr.length // 2
arr[2] = 'c';
arr.length // 3
arr[9] = 'd';
arr.length // 10
arr[1000] = 'e';
arr.length // 1001
上面代码表示,数组的数字键不需要连续,length
属性的值总是比最大的那个整数键大1
。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。
length
属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length
设置的值。
var arr = [ 'a', 'b', 'c' ];
arr.length // 3
arr.length = 2;
arr // ["a", "b"]
上面代码表示,当数组的length
属性设为2(即最大的整数键只能是1)那么整数键2(值为c
)就已经不在数组中了,被自动删除了。
清空数组的一个有效方法,就是将length
属性设为0。
var arr = [ 'a', 'b', 'c' ];
arr.length = 0;
arr // []
如果人为设置length
大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位。
var a = ['a'];
a.length = 3;
a[1] // undefined
上面代码表示,当length
属性设为大于数组个数时,读取新增的位置都会返回undefined
。
如果人为设置length
为不合法的值,JavaScript 会报错。
// 设置负值
[].length = -1
// RangeError: Invalid array length
// 数组元素个数大于等于2的32次方
[].length = Math.pow(2, 32)
// RangeError: Invalid array length
// 设置字符串
[].length = 'abc'
// RangeError: Invalid array length
值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length
属性的值。
var a = [];
a['p'] = 'abc';
a.length // 0
a[2.1] = 'abc';
a.length // 0
上面代码将数组的键分别设为字符串和小数,结果都不影响length
属性。因为,length
属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length
属性保持为0
。
如果数组的键名是添加超出范围的数值,该键名会自动转为字符串。
var arr = [];
arr[-1] = 'a';
arr[Math.pow(2, 32)] = 'b';
arr.length // 0
arr[-1] // "a"
arr[4294967296] // "b"
上面代码中,我们为数组arr
添加了两个不合法的数字键,结果length
属性没有发生变化。这些数字键都变成了字符串键名。最后两行之所以会取到值,是因为取键值时,数字键名会默认转为字符串。
in 运算符
检查某个键名是否存在的运算符in
,适用于对象,也适用于数组。
var arr = [ 'a', 'b', 'c' ];
2 in arr // true
'2' in arr // true
4 in arr // false
上面代码表明,数组存在键名为2
的键。由于键名都是字符串,所以数值2
会自动转成字符串。
注意,如果数组的某个位置是空位,in
运算符返回false
。
var arr = [];
arr[100] = 'a';
100 in arr // true
1 in arr // false
上面代码中,数组arr
只有一个成员arr[100]
,其他位置的键名都会返回false
。
for…in 循环和数组的遍历
for...in
循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象。
var a = [1, 2, 3];
for (var i in a) {
console.log(a[i]);
}
// 1
// 2
// 3
但是,for...in
不仅会遍历数组所有的数字键,还会遍历非数字键。
var a = [1, 2, 3];
a.foo = true;
for (var key in a) {
console.log(key);
}
// 0
// 1
// 2
// foo
上面代码在遍历数组时,也遍历到了非整数键foo
。所以,不推荐使用for...in
遍历数组。
数组的遍历可以考虑使用for
循环或while
循环。
var a = [1, 2, 3];
// for循环
for(var i = 0; i < a.length; i++) {
console.log(a[i]);
}
// while循环
var i = 0;
while (i < a.length) {
console.log(a[i]);
i++;
}
var l = a.length;
while (l--) {
console.log(a[l]);
}
上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。
数组的forEach
方法,也可以用来遍历数组,详见《标准库》的 Array 对象一章。
var colors = ['red', 'green', 'blue'];
colors.forEach(function (color) {
console.log(color);
});
// red
// green
// blue
数组的空位
当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。
var a = [1, , 1];
a.length // 3
上面代码表明,数组的空位不影响length
属性。
需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的。
var a = [1, 2, 3,];
a.length // 3
a // [1, 2, 3]
上面代码中,数组最后一个成员后面有一个逗号,这不影响length
属性的值,与没有这个逗号时效果一样。
数组的空位是可以读取的,返回undefined
。
var a = [, , ,];
a[1] // undefined
使用delete
命令删除一个数组成员,会形成空位,并且不会影响length
属性。
var a = [1, 2, 3];
delete a[1];
a[1] // undefined
a.length // 3
上面代码用delete
命令删除了数组的第二个元素,这个位置就形成了空位,但是对length
属性没有影响。也就是说,length
属性不过滤空位。所以,使用length
属性进行数组遍历,一定要非常小心。
数组的某个位置是空位,与某个位置是undefined
,是不一样的。如果是空位,使用数组的forEach
方法、for...in
结构、以及Object.keys
方法进行遍历,空位都会被跳过。
var a = [, , ,];
a.forEach(function (x, i) {
console.log(i + '. ' + x);
})
// 不产生任何输出
for (var i in a) {
console.log(i);
}
// 不产生任何输出
Object.keys(a)
// []
如果某个位置是undefined
,遍历的时候就不会被跳过。
var a = [undefined, undefined, undefined];
a.forEach(function (x, i) {
console.log(i + '. ' + x);
});
// 0. undefined
// 1. undefined
// 2. undefined
for (var i in a) {
console.log(i);
}
// 0
// 1
// 2
Object.keys(a)
// ['0', '1', '2']
这就是说,空位就是数组没有这个元素,所以不会被遍历到,而undefined
则表示数组有这个元素,值是undefined
,所以遍历不会跳过。
类似数组的对象
如果一个对象的所有键名都是正整数或零,并且有length
属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object)。
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
obj[0] // 'a'
obj[1] // 'b'
obj.length // 3
obj.push('d') // TypeError: obj.push is not a function
上面代码中,对象obj
就是一个类似数组的对象。但是,“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj
没有数组的push
方法,使用该方法就会报错。
“类似数组的对象”的根本特征,就是具有length
属性。只要有length
属性,就可以认为这个对象类似于数组。但是有一个问题,这种length
属性不是动态值,不会随着成员的变化而变化。
var obj = {
length: 0
};
obj[3] = 'd';
obj.length // 0
上面代码为对象obj
添加了一个数字键,但是length
属性没变。这就说明了obj
不是数组。
典型的“类似数组的对象”是函数的arguments
对象,以及大多数 DOM 元素集,还有字符串。
// arguments对象
function args() { return arguments }
var arrayLike = args('a', 'b');
arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false
// DOM元素集
var elts = document.getElementsByTagName('h3');
elts.length // 3
elts instanceof Array // false
// 字符串
'abc'[1] // 'b'
'abc'.length // 3
'abc' instanceof Array // false
上面代码包含三个例子,它们都不是数组(instanceof
运算符返回false
),但是看上去都非常像数组。
数组的slice
方法可以将“类似数组的对象”变成真正的数组。
var arr = Array.prototype.slice.call(arrayLike);
除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()
把数组的方法放到对象上面。
function print(value, index) {
console.log(index + ' : ' + value);
}
Array.prototype.forEach.call(arrayLike, print);
上面代码中,arrayLike
代表一个类似数组的对象,本来是不可以使用数组的forEach()
方法的,但是通过call()
,可以把forEach()
嫁接到arrayLike
上面调用。
下面的例子就是通过这种方法,在arguments
对象上面调用forEach
方法。
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同于 for 循环
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call
遍历。
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
注意,这种方法比直接使用数组原生的forEach
要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach
方法。
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同于 for 循环
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call
遍历。
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
注意,这种方法比直接使用数组原生的forEach
要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach
方法。
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
console.log(chr);
});
// a
// b
// c
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同于 for 循环
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call
遍历。
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同于 for 循环
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call
遍历。
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
注意,这种方法比直接使用数组原生的forEach
要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach
方法。
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
console.log(chr);
});
// a
// b
// c
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同于 for 循环
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call
遍历。
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
注意,这种方法比直接使用数组原生的forEach
要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach
方法。
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
console.log(chr);
});
// a
// b
// c
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
注意,这种方法比直接使用数组原生的forEach
要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach
方法。
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
console.log(chr);
});
// a
// b
// c
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同于 for 循环
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call
遍历。
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
注意,这种方法比直接使用数组原生的forEach
要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach
方法。
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
console.log(chr);
});
// a
// b
// c
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同于 for 循环
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call
遍历。
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
注意,这种方法比直接使用数组原生的forEach
要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach
方法。
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
console.log(chr);
});
// a
// b
// c