0%

JavaScript 权威指南 07:数组

数组是值的有序集合,其中的值叫作元素。每个元素有一个数值表示的位置,叫作索引。这篇文章将详细介绍 JavaScript 的数组。

JavaScript 数组简介

  • JavaScript 数组是无类型限制的,即数组中的元素可以是任意类型,同一数组的不同元素也可以是不同的类型
  • JavaScript 数组是基于零且使用 32 位数值索引的,第一个元素的索引为 0
  • JavaScript 数组是动态的,它们会按需增大或缩小,因此创建数组时无须声明一个固定大小,也无须在大小变化时重新为它们分配空间
  • JavaScript 数组可以是稀疏的,即元素不一定具有连续的索引,中间可能有间隙
  • 每个 JavaScript 数组都有 length 属性。对于非稀疏数组,这个属性保存数组中元素的个数。对于稀疏数组,length 大于所有元素的最高索引

JavaScript 数组是一种特殊的 JavaScript 对象,因此数组索引更像是属性名,只不过碰巧是整数而已。实现通常对数组会进行特别优化,从而让访问数值索引的数组元素明显快于访问常规的对象属性。

数组从 Array.prototype 继承属性,这个原型上定义了很多数组操作方法。

创建数组

数组字面量

创建数组最简单的方式就是使用数组字面量。数组字面量其实就是一对方括号中逗号分隔的数组元素的列表。

1
2
3
> let empty = []
> let primes = [2, 3, 5, 7, 11]
> let mixed = [1.1, true, "a", ]

数组字面量中的值不需要是常量,可以是任意表达式。数组字面量可以包含对象字面量或其他数组字面量。

1
2
3
> let base = 1024;
> let table = [base, base+1, base+2]
> let b = [[1, {x:1, y:2}], [2, {x:3, y:4}]]

如果数组字面量中连续包含多个逗号,且逗号之间没有值,则这个数组就是稀疏的。这些省略了值的数组元素并不存在,但按照索引查询它们时又会返回 undefined:

1
2
3
4
5
6
7
8
> let count = [1,,,,5]
undefined
> count.length
5
> let e = [,,,]
undefined
> e.length
3

由于数组字面量语法允许末尾出现逗号,因此 [,,,] 的长度是 3 不是 4。

扩展操作符

在 ES6 及之后的版本中,可以使用扩展操作符 ...(实际上并不是真的操作符)在一个数组字面量中包含另一个数组的元素:

1
2
3
4
> let a1 = [1, 2, 3]
> let a2 = [0, ...a1, 4, 5]
> a2
[ 0, 1, 2, 3, 4, 5 ]

扩展操作符是创建数组副本的一种便捷方式:

1
2
3
4
5
6
7
8
9
10
> let source = [1, 2, 3]
> let dest = [...source]
> dest
[ 1, 2, 3 ]
> source[0] = 0
0
> source[0]
0
> dest[0]
1

扩展操作符适用于任何可迭代对象,字符串是可迭代对象,因此可以使用扩展操作符把任意字符串转换为单个字符的数组:

1
2
3
4
> let t = [..."test"]
undefined
> t
[ 't', 'e', 's', 't' ]

集合对象也是可迭代的,因此要去除数组中的重复元素,一种便捷方式就是先把数组转换为集合,然后再使用扩展操作符把这个集合转换回数组:

1
2
3
4
5
6
7
> let letters = [..."hello world"]
> [...new Set(letters)]
[
'h', 'e', 'l',
'o', ' ', 'w',
'r', 'd'
]

Array 构造函数

另一种创建数组的方式是使用 Array() 构造函数。有三种方式可以调用这个构造函数:

  • 不传参数调用,创建一个空数组,等价于 []
  • 传入一个参数,指定数组的长度。这样调用可以提前为数组分配空间,但是数组中不会存储任何值,访问数组元素都是未定义的
  • 传入两个或更多个数组元素,或传入一个或多个非数值元素,此时参数会成为新数组的元素。
1
2
3
4
5
6
7
8
9
10
11
12
> let a = new Array();

> let a2 = new Array(10);
undefined
> a2[0]
undefined

> let a3 = new Array(10, 10, "test");
> a3[0]
10
> a3[2]
'test'

Array.of()

从上文对 Array 构造函数的讨论可以看出:

  • 当使用一个数值参数调用 Array 参数时,这个参数指定的是数组的长度
  • 在使用一个以上的数值参数时,这些参数则会成为新数组的元素

因此使用 Array() 构造函数无法创建只包含一个数值元素的数组。在 ES6 中,Array.of() 函数可以解决这个问题。这是个工厂方法,可以使用其参数值(无论多少个)作为数组元素来创建并返回新数组:

1
2
3
4
5
> let a1 = Array.of()
> let a2 = Array.of(1)
> let a3 = Array.of(1, 2, 3)
> [a1.length, a2.length, a3.length]
[ 0, 1, 3 ]

Array.from()

Array.from() 是 ES6 新增的另一个工厂方法。这个方法期待一个可迭代对象或类数组对象作为其第一个参数,并返回包含该对象元素的新数组。如果传入可迭代对象,Array.from(iterable) 与使用扩展操作符 [...iterable] 一样。如下也可以快速创建数组副本:

1
let copy = Array.from(source)

Array.from() 确实很重要,因为它定义了一种给类数组对象创建真正的数组副本的机制。

Array.from() 也接受可选的第二个参数。如果给第二个参数传入了一个函数,那么在构建新数组时,源对象的每个元素都会传入这个函数,这个函数的返回值将代替原始值成为新数组的元素(类似于数组的 map 方法,但是效率更高)。

读写数组元素

可以使用 [​] 操作符访问数组元素,方括号左侧应该是一个对数组的引用,方括号内应该是一个具有非负整数值的表达式。

数组特殊的地方在于,只要你使用小于 2**32-1 的非负整数作为属性名,数组就会自动为你维护 length 属性的值。

1
2
3
4
5
6
7
8
9
> let a = [1]
> a[0]
1
> a[1] = 2
2
> a[2] = 3
3
> a.length
3

数组是一种特殊的对象。用于访问数组元素的方括号与用于访问对象属性的方括号是类似的。JavaScript 会将数值索引转换为字符串,即索引 1 会变成字符串 "1"​,然后再将这个字符串作为属性名。这个从数值到字符串的转换没什么特别的,使用普通对象也一样:

1
2
3
4
5
> let o = {}
> o[1] = "one"
'one'
> o["1"]
'one'

所有索引都是属性名,但只有介于 0 和 2**32-2 之间的整数属性名才是索引。所有数组都是对象,可以在数组上以任意名字创建属性。只不过,如果这个属性是数组索引,数组会有特殊的行为,即自动按需更新其 length 属性

  • 可以使用负数或非整数值来索引数组。此时,数值会转换为字符串,而这个字符串会作为属性名
  • 如果你碰巧使用了非负整数的字符串来索引数组,那这个值会成为数组索引,而不是对象属性。同样,如果使用了与整数相等的浮点值也是如此
  • 由于数组索引其实就是一种特殊的对象属性,所以 JavaScript 数组没有所谓 越界 错误。查询任何对象中不存在的属性都不会导致错误,只会返回 undefined。数组作为一种特殊对象也是如此
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> let a = []
> a[-1] = 1
1
> a[1] = 2
2
> a[2.00] = 2
2
> a.length
3
> a
[ <1 empty item>, 2, 2, '-1': 1 ]

> a[-1]
1
> a["-1"]
1
> a[100]
undefined

稀疏数组

稀疏数组是指数组元素索引并不是从 0 开始的连续值。正常情况下,数组的 length 属性表明数组中元素的个数。如果数组是稀疏的,则 length 属性的值会大于元素个数。

1
2
3
4
5
6
7
8
> let a = new Array(5)
> a.length
5

> a[1000] = 0;
0
> a.length
1001

如果省略数组字面量中的一个值​,也会得到稀疏数组,被省略的元素是不存在的

1
2
3
4
5
6
7
8
> a = [1,,,,5]
[ 1, <3 empty items>, 5 ]
> a.length
5
> 0 in a
true
> 2 in a
false

数组的长度

每个数组都有 length 属性,正是这个属性让数组有别于常规的 JavaScript 对象。

  • 对于非稀疏数组,length 属性就是数组中元素的个数。这个值比数组的最高索引大 1:
  • 对于稀疏数组,length 属性会大于元素个数,也可以说稀疏数组的 length 值一定大于数组中任何元素的索引
  • 因此,数组无论稀疏与否,其中任何元素的索引都不会大于或等于数组的 length

为了维护这种行为:

  • 如果给一个索引为 i 的数组元素赋值,而 i 大于或等于数组当前的 length,则数组的 length 属性会被设置为 i+1
  • 如果将 length 属性设置为一个小于其当前值的非负整数 n,则任何索引大于或等于 n 的数组元素都会从数组中被删除:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> a = [1, 2, 3, 4, 5]
[ 1, 2, 3, 4, 5 ]
> a.length
5
> a.length = 3
3
> a
[ 1, 2, 3 ]
> a.length = 1
1
> a
[ 1 ]

> a.length = 100
100
> a
[ 1, <99 empty items> ]
> a[2]
undefined

也可以把数组的 length 属性设置为一个大于其当前值的值。这样做并不会向数组中添加新元素,只会在数组末尾创建一个稀疏的区域。

添加和删除数组元素

数组添加元素的最简单方式就是给它的一个新索引赋值,也可以使用 push() 方法在数组末尾添加一个或多个元素。向数组 a 中推入一个值等同于把这个值赋给 a[a.length]​

1
2
3
4
5
6
7
8
> a = []
[]
> a.push("test")
> a.push("test2")
> a.length
2
> a
[ 'test', 'test2' ]
  • pop() 方法删除数组最后一个元素并返回该元素,同时导致数组长度减 1
  • unshift() 方法在数组开头插入值,它将已有的数组元素移动到更高索引位
  • shift() 方法删除数组第一个元素并返回该元素,让数组长度减 1 并将所有元素移动到低一位的索引

可以使用 delete 操作符删除数组元素:

1
2
3
4
5
6
7
8
> a = [1, 2, 3]
[ 1, 2, 3 ]
> delete a[0]
true
> a
[ <1 empty item>, 2, 3 ]
> a.length
3
  • 删除数组元素类似于(但不完全等同于)给该元素赋 undefined 值
  • 对数组元素使用 delete 操作符不会修改 length 属性,也不会把高索引位的元素向下移动来填充被删除属性的空隙
  • delete 从数组中删除元素后,数组会变稀疏

之前说过,把数组 length 属性设置成一个新长度值,也可以从数组末尾删除元素。

splice() 是一个可以插入、删除或替换数组元素的通用方法。这个方法修改 length 属性并按照需要向更高索引或更低索引移动数组元素。

迭代数组

到 ES6 为止,遍历一个数组(或任何可迭代对象)的最简单方式就是使用 for/of 循环:

1
2
3
4
5
> a = [1, 2, 3]
> for (let v of a) console.log(v)
1
2
3

for/of 循环使用的内置数组迭代器按照升序返回数组的元素。对于稀疏数组,这个循环没有特殊行为,凡是不存在的元素都返回 undefined

1
2
3
4
5
6
7
8
> a = []
[]
> a[100] = 100
100
> let count = 0
> for (let v of a) count++
> count
101
  • 如果要对数组使用 for/of 循环,并且想知道每个数组元素的索引,可以使用数组的 entries() 方法和解构赋值。
  • 另一种迭代数组的推荐方式是使用 forEach()。它并不是一种新的 for 循环,而是数组提供的一种用于自身迭代的函数式方法。需要给 forEach() 传一个函数,然后 forEach() 会用数组的每个元素调用一次这个函数。与 for/of循环不同,forEach()能够感知稀疏数组,不会对没有的元素数组调用函数。
1
2
3
4
5
> a = [1, 2, 3]
> let sum = 0
> a.forEach( v => { sum += v; })
> sum
6
  • 使用老式的 for 循环也可以遍历数组。有时候为了提高性能,只会读取一次数组长度,而不是在每个迭代中都读取一次

多维数组

JavaScript 通过数组的数组来模拟多维数组:

1
2
3
4
5
6
7
8
9
10
11
12
let table = new Array(10);
for (let i = 0; i < table.length; i++) {
table[i] = new Array(10);
}

for (let row = 0; row < table.length; row++) {
for (let col = 0; col < table[row].length; col++) {
table[row][col] = row * col;
}
}

console.log(table[5][7]);

数组方法

接下来介绍 Array 类定义的方法。

数组迭代器方法

数组迭代器方法用于迭代数组元素,它们会按照顺序把数组的每个元素传给我们提供的函数,可便于对数组进行迭代、映射、过滤、测试和归并。

  • 所有这些方法都接收一个函数作为第一个参数,并且对数组的每个元素(或某些元素)都调用一次这个函数
  • 如果数组是稀疏的,则不会对不存在的数组元素调用传入的这个函数
  • 多数情况下,我们提供的这个函数被调用时都会收到 3 个参数,分别是数组元素的值、数组元素的索引和数组本身
  • 多数迭代器方法都接收可选的第二个参数。如果指定这个参数,则第一个函数在被调用时就好像它是第二个参数的方法一样。也就是说,传入的第二个参数会成为作为第一个参数传入的函数内部的 this 值

forEach() 方法迭代数组的每个元素,并对每个元素都调用一次我们指定的函数:

  • forEach() 在调用这个函数时会给它传 3 个参数:数组元素的值、数组元素的索引和数组本身
  • 如果只关心数组元素的值,可以把函数写成只接收一个参数,即忽略其他参数
  • forEach() 并未提供一种提前终止迭代的方式

map() 方法把调用它的数组的每个元素分别传给我们指定的函数,返回这个函数的返回值构成的数组。

  • map() 返回一个新数组,并不修改调用它的数组
  • 如果数组是稀疏的,则缺失元素不会调用我们的函数,但返回的数组也会与原始数组一样稀疏:长度相同,缺失的元素也相同
1
2
3
> let a = [1, 2, 3]
> a.map(x => x*2)
[ 2, 4, 6 ]

filter() 方法返回一个数组,该数组包含调用它的数组的子数组:

  • 传给这个方法的函数应该是个断言函数,即返回 true 或 false 的函数
  • 如果函数返回 true 或返回的值转换为 true,则传给这个函数的元素就是 filter() 最终返回的子数组的成员
  • filter() 会跳过稀疏数组中缺失的元素,它返回的数组始终是稠密的。因此如下方法直接清理稀疏数组中的空隙:
1
let dense = sparse.filter(() => true);
  • 如果想清理空隙,同时删除值为 null 或者 undeinfed 的元素,则可以这样:
1
let a = a.filter(x => x != null&& x != undefined);

find() 和 findIndex() 方法与 filter() 类似,表现在它们都遍历数组,寻找断言函数返回真值的元素。但是这两个方法会在断言函数找到第一个元素时停止迭代:

  • find() 返回匹配的元素,findIndex()返回匹配元素的索引
  • 如果没有找到匹配的元素,则 find() 返回 undefined,而 findIndex() 返回 -1

every() 和 some() 方法是数组断言方法,即它们会对数组元素调用我们传入的断言函数,最后返回 true 或 false。

  • every() 方法当且仅当断言函数对数组的所有元素都返回 true 时才返回 true
  • Some() 只要数组元素中有一个让断言函数返回 true 它就返回 true
  • every() 和 some() 都会已经知道最终结果的时候,停止迭代数组

reduce() 和 reduceRight() 方法使用我们指定的函数归并数组元素,最终产生一个值。

  • reduce() 接收两个参数。第一个参数是执行归并操作的函数。这个归并函数的任务就是把两个值归并或组合为一个值并返回这个值。第二个参数是可选的,是传给归并函数的初始值。
  • reduce 接受的函数参数,其第一个参数是目前为止归并操作的累计结果。在第一次调用时,它的值是 reduce() 方法的第二个参数(如果提供了的话)。如果 reduce() 没有提供第二个值,reduce() 会使用数组的第一个元素作为初始值,这意味着首次调用归并函数将以数组的第一和第二个元素作为其第一和第二个参数
  • 值、索引和数组本身作为函数参数的第二、第三和第四个参数
  • 如果不传初始值参数,在空数组上调用 reduce() 会导致 TypeError
  • 如果调用它时只有一个值,比如用只包含一个元素的数组调用且不传初始值,或者用空数组调用但传了初始值,则 reduce() 直接返回这个值,不会调用归并函数
  • reduceRight() 与 reduce() 类似,只不过是从高索引向低索引(从右到左)处理数组
  • 无论 reduce() 还是 reduceRight() 都不接收用于指定归并函数this值的可选参数。它们用可选的初始值参数取代了这个值
1
2
3
4
5
6
> a.reduce((x, y) => x + y, 0)
15
> a.reduce((x, y) => x * y, 1)
120
> a.reduce((x, y) => x > y? x : y)
5

使用 flat() 和 flatMap() 打平数组

flat() 方法用于创建并返回一个新数组,这个新数组包含与它调用 flat() 的数组相同的元素,只不过其中任何本身也是数组的元素会被 打平 填充到返回的数组中。

  • 在不传参调用时,flat() 会打平一级嵌套
  • 如果想打平更多层级,需要给 flat() 传一个数值参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
> [1, [2, 3], [4, 5, 6]].flat()
[ 1, 2, 3, 4, 5, 6 ]

> b.flat()
[ 1, 2, [ 3, [ 4 ] ] ]
> b.flat(1)
[ 1, 2, [ 3, [ 4 ] ] ]
> b.flat(2)
[ 1, 2, 3, [ 4 ] ]
> b.flat(3)
[ 1, 2, 3, 4 ]
> b.flat(4)
[ 1, 2, 3, 4 ]

flatMap() 方法与 map() 方法相似,只不过返回的数组会自动被打平,就像传给了 flat() 一样。因此 a.flatMap(f) 等同于 a.map(f).flat(),但是效率更高。

1
2
3
4
> let phrases = ["hello world", "the definitive guide"]
> let w = phrases.flatMap(phrase => phrase.split(" "))
> w
[ 'hello', 'world', 'the', 'definitive', 'guide' ]

使用 concat() 添加数组

concat() 方法创建并返回一个新数组,新数组包含调用 concat() 方法的数组的元素,以及传给 concat() 的参数。如果这些参数中有数组,则拼接的是它们的元素而非数组本身。但是 concat() 不会递归打平数组的数组。

1
2
3
4
5
6
7
8
9
10
11
> let a = [1, 2, 3]
> a.concat([4, 5])
[ 1, 2, 3, 4, 5 ]
> a.concat([4, 5], [6, 7], [9, [10]])
[
1, 2, 3,
4, 5, 6,
7, 9, [ 10 ]
]
> a
[ 1, 2, 3 ]

concat() 并不修改调用它的数组,concat() 会创建调用它的数组的副本,因此它的开销更大。

通过 push()、pop()、shift() 和 unshift() 实现栈和队列操作

push() 和 pop() 方法可以把数组作为栈来操作。

  • push()方法用于在数组末尾添加一个或多个新元素,并返回数组的新长度
  • pop() 用于删除数组最后面的元素,减少数组长度,并返回删除的值
  • push()方法不会打平传入的数组,如果想把一个数组中的所有元素都推送到另一个数组中,可以使用扩展操作符
1
2
3
> let values = [1, 2, 3];
> [].push(...values)
3

unshift() 和 shift() 方法与 push() 和 pop() 很类似,只不过它们是从数组开头而非末尾插入和删除元素。

  • unshift() 用于在数组开头添加一个或多个元素,已有元素的索引会相应向更高索引移动,并返回数组的新长度
  • shift() 删除并返回数组的第一个元素,所有后续元素都会向下移动一个位置,以占据数组开头空出的位置

需要额外注意,在给 unshift() 传多个参数时,这些参数会一次性插入数组。这意味着一次插入与多次插入之后的数组顺序不一样

1
2
3
4
5
6
7
8
9
10
11
> let a1 = []
> a1.unshift(1, 2, 3)
3
> a1
[ 1, 2, 3 ]
> let a2 = []
> a2.unshift(1)
> a2.unshift(2)
> a2.unshift(3)
> a2
[ 3, 2, 1 ]

使用 slice()、splice() 和 fill() 和 copyWithin()

slice() 方法返回一个数组的切片(slice)或者子数组:

  • 这个方法接收两个参数,分别用于指定要返回切片的起止位置。
  • 返回的数组包含第一个参数指定的元素,以及所有后续元素,直到(但不包含)第二个参数指定的元素
  • 如果只指定一个参数,返回的数组将包含从该起点开始直到数组末尾的所有元素
  • 如果任何一个参数是负值,则这个值相对于数组长度指定数组元素,因此 -1 指定数组的最后一个元素

splice() 是一个对数组进行插入和删除的通用方法,splice() 会修改调用它的数组。

  • splice()的第一个参数指定插入或删除操作的起点位置
  • 第二个参数指定要从数组中删除(切割出来)的元素个数。如果省略第二个参数,从起点元素开始的所有数组元素都将被删除
  • splice() 返回被删除元素的数组,如果没有删除元素返回空数组
  • splice() 的前两个参数指定要删除哪些元素。这两个参数后面还可以跟任意多个参数,表示要在第一个参数指定的位置插入到数组中的元素。
1
2
3
4
5
> let a = [1, 2, 3, 4, 5]
> a.splice(2, 1, "a", "b")
[ 3 ]
> a
[ 1, 2, 'a', 'b', 4, 5 ]

fill() 方法将数组的元素或切片设置为指定的值。它会修改调用它的数组,也返回修改后的数组:

  • fill()的第一个参数是要把数组元素设置成的值
  • 可选的第二个参数指定起始索引,如果省略则从索引 0 开始填充
  • 可选的第三个参数指定终止索引,到这个索引为止(但不包含)的数组元素会被填充
  • 如果省略第三个参数,则从起始索引开始一直填充到数组末尾。
  • 传入负值相对于数组末尾指定索引
1
2
3
4
> a = new Array(100)
[ <100 empty items> ]
> a.fill(10, 0, 10)
[ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, <90 empty items> ]

copyWithin() 把数组切片复制到数组中的新位置,它会就地修改数组并返回修改后的数组,但不会改变数组的长度:

  • 第一个参数指定要把第一个元素复制到的目的索引
  • 第二个参数指定要复制的第一个元素的索引,如果省略,则默认为 0
  • 第三个参数指定要复制的元素切片的终止索引。如果省略,则使用数组的长度
  • 从起始索引到(但不包含)终止索引的元素会被复制
  • 传入负值相对于数组末尾指定索引
  • 即使来源和目标区域有重叠,复制也是正确的
1
2
3
4
> a = [1, 2, 3, 4, 5]
[ 1, 2, 3, 4, 5 ]
> a.copyWithin(1, -2, -1)
[ 1, 4, 3, 4, 5 ]

数组索引与排序方法

indexOf()lastIndexOf() 从数组中搜索指定的值并返回第一个找到的元素的索引,如果没找到则返回 -1。

  • indexOf() 从前到后搜索数组
  • 而 lastIndexOf() 从后向前搜索数组
  • 使用 === 操作符比较它们的参数和数组元素
  • indexOf() 和 lastIndexOf() 都接收第二个可选的参数,指定从哪个位置开始搜索。可以是负值

ES2016 的 includes() 方法接收一个参数,如果数组包含该值则返回 true,否则返回 false

  • 相比于 indexOf()=== 判断,includes() 使用稍微不同的相等测试,它认为 NaN 与自身相等。这意味着 indexOf() 无法检测数组中的 NaN 值,但 includes() 可以

sort() 对数组元素就地排序并返回排序后的数组:

  • 在不传参调用时,sort() 按字母顺序对数组元素排序(如有必要,临时把它们转换为字符串)。
  • 如果数组包含未定义的元素,它们会被排到数组末尾
  • 要对数组元素执行非字母顺序的排序,必须给 sort() 传一个比较函数作为参数
1
2
3
4
5
6
7
8
> a = [22, 111, 3]
[ 22, 111, 3 ]
> a.sort()
[ 111, 22, 3 ]
> a.sort((a, b) => a - b)
[ 3, 22, 111 ]
> a.sort((a, b) => b - a)
[ 111, 22, 3 ]

reverse() 方法反转数组元素的顺序,并返回反序后的数组,这个反序是就地反序。

1
2
3
4
> a
[ 111, 22, 3 ]
> a.reverse()
[ 3, 22, 111 ]

数组到字符串的转换

Array 类定义了 3 个把数组转换为字符串的方法,通常可以用在记录日志或错误消息的时候。

  • join() 方法把数组的所有元素转换为字符串,然后把它们拼接起来并返回结果字符串。可以指定一个可选的字符串参数,用于分隔结果字符串中的元素。默认为逗号
  • 与任何 JavaScript 对象一样,数组也有 toString() 方法。对于数组而言,这个方法的逻辑与没有参数的 join() 方法一样
  • toLocaleString() 是 toString() 的本地化版本。它调用 toLocaleString() 方法将每个数组元素转换为字符串,然后再使用(实现定义的)当地分隔符字符串来拼接结果字符串
1
2
3
4
5
6
> a.join()
'1,2,3'
> a.toString()
'1,2,3'
> a.toLocaleString()
'1,2,3'

静态数组函数

Array 类也定义了 3 个静态函数,包括之前介绍的 Array.of()Array.from(),还有一个是 Array.isArray(),用于判断某个值师傅为数组。

1
2
3
4
> Array.isArray([])
true
> Array.isArray({})
false

类数组对象

总结一下,JavaScript 数组具有一些其他对象不具备的特殊特性:

  • 数组的 length 属性会在新元素加入时自动更新
  • 设置 length 为更小的值会截断数组
  • 数组从 Array.prototype 继承有用的方法
  • Array.isArray() 对数组返回 true

这些特性让 JavaScript 数组与常规对象有了明显区别。但是,这些特性并非定义数组的本质特性。事实上,只要对象有一个数值属性 length,而且有相应的非负整数属性,那就完全可以视同为数组

实践当中,我们偶尔会碰到 类数组 对象。虽然不能直接在它们上面调用数组方法或期待 length 属性的特殊行为,但仍然可以通过写给真正数组的代码来遍历它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let a = {};

let i = 0;
while (i < 10) {
a[i] = i * i;
i++;
}

a.length = i;

let total = 0;
for (let j = 0; j < a.length; j++) {
total += a[j];
}

console.log(total); // => 285
console.log(Array.isArray(a)) //=> false

这段代码为一个常规对象添加属性,让它成为一个类数组对象,然后再遍历得到的伪数组的 元素

在客户端 JavaScript 中,很多操作 HTML 文档的方法(比如 document.querySelectorAll())都返回类数组对象。

多数 JavaScript 数组方法有意地设计成了泛型方法,因此除了真正的数组,同样也可以用于类数组对象。但由于类数组对象不会继承 Array.prototype,所以无法直接在它们上面调用数组方法。为此,可以使用 Function.call() 方法:

1
2
3
4
5
6
7
> let a = {"0":1, "1":2, "2":3, length:3}
> Array.prototype.join.call(a, "+")
'1+2+3'

// 得到一个真正的数组
> Array.prototype.slice.call(a, 0)
[ 1, 2, 3 ]

作为数组的字符串

JavaScript 字符串的行为类似于 UTF-16 Unicode 字符的只读数组。除了使用 charAt() 方法访问个别字符,还可以使用方括号语法:

1
2
3
4
5
> let s = "test";
> s.charAt(0)
't'
> s[0]
't'

字符串与数组的行为类似也意味着我们可以对字符串使用泛型的字符串方法,例如:

1
2
> Array.prototype.slice.call("test", " ")
[ 't', 'e', 's', 't' ]

一定要记住,字符串是不可修改的值,因此在把它们当成数组来使用时,它们是只读数组。像 push()、sort()、reverse() 和 splice() 这些就地修改数组的数组方法,对字符串都不起作用。但尝试用数组方法修改字符串并不会导致错误,只会静默失败(在 Node 中实际测试会失败):

1
2
3
4
> Array.prototype.push.call("test", " ")
Uncaught:
TypeError: Cannot assign to read only property 'length' of object '[object String]'
at String.push (<anonymous>)