跳到主要内容

数组

数组 是值的有序集合,其中的值称为元素,每个元素有一个数值表示的位置称为索引。

JS 数组是无类型限制的,即同一数组中不同元素可以是不同类型,从而可以构建复杂的数据结构。

JS 数组是基于零且使用 32 位数值索引的,第一个元素的索引是 0 ,最大可能的索引值是 232-2 ,因为数组最大长度为 232-1 。JS 数组是动态的,创建时不需要(跟别的语言一样)声明一个固定大小。JS 数组可以是稀疏的,即元素不一定具有连续的索引,中间可能留空。对于稀疏数组,length 属性并非实际元素的个数,但会大于所有元素中的最高索引。

JS 数组是种特殊的 JS 对象,因此数组索引更像是属性名。一般的实现会对数组进行特别优化,从而在访问数值索引的数组元素明显快访问常规的对象属性。

数组从 Array.prototype 继承属性,该原型定义了很多数组方法。其中很多方法是泛型的,可以用在 "类数组对象" 。JS 字符串的行为也类似数组。

ES6 中还新增了定型数组 TypedArray ,它具有固定长度和元素类型,因此有极高的性能,且支持对二进制数据的字节级访问。

创建数组

数组字面量

数组字面量用逗号分隔元素,中括号包裹,允许末尾出现逗号:

let primes = [2, 3, 5, 7, 11,]    // 5 个元素
let arr = [1.1, true, "a"] // 3 个不同类型的元素

如果逗号间没有值,会留空,数组便成了稀疏数组:

let arr = [1, , 3]    // => (3) [1, 空, 3]

数组展开

ES6 及后,可以用展开语法在一个数组字面量中包含另一个数组的元素:

let a = [1, 2, 3]
let b = [0, ...a, 4] // => (5) [0, 1, 2, 3, 4]

展开语法也可成为浅拷贝数组的便捷方式之一:

let a = [1, 2, 3]
let b = [...a] // => (3) [1, 2, 3]

展开语法适用于任何可迭代对象(可用 for-of 遍历)。字符串是可迭代对象:

let letters = [..."hello"]  // => (5) ["h", "e", "l", "l", "o"]

可以用集合 Set 对数组进行去重:

let arr = [1, 1, 2, 3, 2];
[...new Set(arr)] // => (3) [1, 2, 3]

Array 构造函数

有三种方式调用 Array 构造函数:

  • 无参数。等价于数组字面量 []

    let a = new Array()       // => (0) []
  • 只传入一个数组长度参数。该参数会定义到数组对象的 length 属性,但数组为空:

    let a = new Array(10)     // => (10) [空×10]

    长度应当在 [0, 232-1] 区间内,否则会抛出 RangeError 。

  • 传入两个及以上的元素参数,或者传入一个非数值元素:

    let a = new Array(3, 4)   // => (2) [3, 4]
    let b = new Array(true) // => (1) [true]

根据规范,new Array()Array() 直接调用是等价的,因此可以不加 new 关键字。

搭配展开语法可以创建固定长度数组,且非稀疏数组:

[...Array(10)]  // 等价于 `Array(10).fill()`

Array 工厂方法

ES6 新增了 Array.of()Array.from() 这两个类方法。

Array.of() 会将所有参数作为新数组的元素,和 Array 构造函数类似:

Array.of()            // => (0) []
Array.of(1) // => (1) [1]
Array.of("h", true) // => (2) ["h", true]

Array.from() 则可根据传入的 可迭代对象类数组对象 创建新数组:

类数组对象不是数组对象,但也有 length 属性,而且每个属性名也是整数。

Array.from("hello")   // => (5) ["h", "e", "l", "l", "o"]

传入可迭代对象时,行为与展开语法 [...iterable] 一样。

Array.from() 也接受第二个映射函数参数,用来对每个元素进行转换:

Array.from([1, 2, 3], e => e + 1)     // => (3) [2, 3, 4]

读写数组元素

可以用 [] 操作符访问数组元素,在赋值时,数组会自动维护 length 值:

let a = [1, 2, 3]
a[1] // => 2
a["2"] // => 3
a[4] = 5 // => (5) [1, 2, 3, 空, 5]

数组索引应当在 [0, 232-1) 区间内,否则会视为常规对象属性(数组也是特殊的对象)。

数组长度 & 稀疏数组

一般 length 为数组元素个数,如果手动指定 length 值,则会对数组进行末尾留空或删除元素:

let a = [1, 2, 3]
a.length = 5 // => (5) [1, 2, 3, 空×2]
a.length = 2 // => (2) [1, 2]

但当 length 属性大于元素个数时,该数组则为稀疏数组,此时数组中会有留空的索引位置:

let a = new Array(5)      // => (5) [空×10]
0 in a // => false
let b = [1, 2, 3]         // => (3) [1, 2, 3]
b[4] = 5 // => (5) [1, 2, 3, 空, 5]
3 in b // => false

如果一个数组足够稀疏,那么它的元素查询效率会与查询常规对象属性相当。

添加 & 删除元素

使用 push() 方法在原数组末尾添加一个或多个元素,在开头插值用 unshift() 方法。这两个方法都返回数组新的长度。

使用 pop() 方法会删除原数组的最后一个元素,并返回该元素。而 shift() 会删除第一个元素并返回该元素。

let arr = []
arr.push(1, 2, 3) // => 3 , arr = [1, 2, 3]
arr.pop() // => 3 , arr = [1, 2]
arr.shift() // => 1 , arr = [2]
arr.unshift(0) // => 2 , arr = [0, 2]

这些方法主要是针对 length 修改,即对于稀疏数组,空值依然会视为元素。

注意,使用 delete 删除数组元素不会自动修改 length 属性。不过,直接设置 length 会达到删除末尾元素的效果。

splice() 可以在指定位置删除并替换元素。它的第一个参数为操作位置,第二个参数为向后删除元素的个数,其余参数为要替换的元素列表,返回值为被删除的元素数组:

let arr = [1, 1, 1, 2, 2, 2]
arr.splice(2, 2, 3, 4, 5) // => (2) [1, 2]
arr // => (7) [1, 1, 3, 4, 5, 2, 2]

遍历数组

到 ES6 为止,遍历数组(或任何可迭代对象)的最简单方法为使用 for-of 循环:

如果是稀疏数组,对于不存在的元素会返回 undefined

let nums = [1, 2, 3, 4], sum = 0
for (let num of nums) {
sum += num
} // sum = 10

如果是想带上数组索引,可以用 entries() 方法,以及解赋值构语法:

let nums = [1, 2, 3, 4]
for (let [index, num] of nums.entries()) {
nums[index] === num // => true
}

另一种推荐的遍历方式是使用 forEach() 方法。与 for 循环不同的是,它只遍历存在的元素(对于稀疏数组)。forEach() 接收一个处理元素的函数:

处理元素的函数会接收三个可选参数:元素值、元素对应的数组索引、数组自身的引用。

let nums = [1, 2, 3, 4], sum = 0
nums.forEach(num => {
sum += num
}) // sum = 10

当然,也可以用传统的 for 语句:

let nums = [1, 2, 3, 4], sum = 0
for (let i = 0; i < nums.length; i += 1) {
sum += nums[i]
} // sum = 10

在上面这个例子中,它只会读取一次数组长度,而非每个迭代都读一次。

多维数组

JS 不支持真正的多维数组,但可以直接用数组嵌套来模拟。下面这个例子生成了乘法表:

let table = new Array(10)
for (let i = 0; i < table.length; i += 1) {
table[i] = new Array(10)
for (let j = 0; j < table[i].length; j += 1) {
table[i][j] = i * j
}
}
table[3][5] // => 15

数组方法

具体方法使用参阅 Array - MDN

迭代方法

forEach() map() filter() every() some() reduce() reduceRight() find() findIndex()

这些方法只作用于存在的元素,会跳过稀疏数组中缺失的元素。

前 5 个方法均会接收一个函数作为第一个参数,函数有三个可选参数:元素值、元素对应索引、数组本身。而第二个参数为传入函数的 this 值。

some()every() 会在得到确定结果后停止迭代。

reduce()reduceRight() 会接收一个归并函数和一个可选的初始值参数。如果不指定初始值,空数组调用这两个方法会导致 TypeError 。如果数组只有一个元素但没有指定初始值,或者空数组且给定初始值,则不会调用归并函数,并返回这个值。

最后两个方法中,若没找到指定元素,find() 会返回 undefinedfindIndex() 返回 -1 。

元素添加删除

push() pop() shift() unshift() splice()

查找方法

indexOf() lastIndexOf() includes()

前两个方法会返回查找到的第一个元素的位置,没有找到则返回 -1 。匹配使用的是严格相等。可传入第二个参数,为开始搜索的位置,允许负数作为倒数。

字符串也有 indexOf()lastIndexOf() 方法,但如果第二个参数为负数会转为 0 。

includes() 也会使用类似严格相等的比较,不同的是会认为 NaN 与自身相等。

扁平化

flat() flatMap()

没有指定参数时,flat() 会打平一级数组嵌套。如果想要打平更深级数嵌套,可以指定这个参数。

flatMap()map() 类似,不过返回的数组会被一级打平。即 arr.flatMap(fn) 类似于(但效率远高于)arr.map(fn).flat()

键值对

keys() values() entries()

构建新数组

slice() concat() fill() copyWithin()

slice() 数组切片。接收可选的起止位置。参数允许为负数,代表倒数。

concat() 数组拼接新元素。会检查传入的参数,若为数组则一级扁平化。

fill() 数组内容填充。接收三个参数:填充值、起始/终止位置。起止位置允许为负数。

copyWithin() 数组内部内容的复制粘贴。接收三个参数:粘贴位置、拷贝起始/终止位置。起止位置允许为负数。该方法本意为一个高性能方法。

元素顺序

reverse()sort()

这两个方法均修改数组自身。

sort() 若没有指定比较函数,会按字母序对数组元素排序(如有必要会先转为字符串再比较)。比较函数按返回正数、0、负数进行大小判别。

转为字符串

join()

类方法

Array.isArray()Array.from()Array.of()

类数组对象

JS 数组具有一些常规对象不具备的特殊属性:

  • 数组 length 会在一些操作中自动更新;

  • 设置 length 为更小的值会截断数组;

  • 数组从 Array.prototype 继承;

  • Array.isArray() 对数组返回 true

不过,如果一个对象有一个数值属性 length ,且有相应的非负整数属性,则可以视为类数组。在客户端 JS 中,很多操作 HTML 的方法都返回类数组对象。

多数 JS 数组方法有意设计成了泛型方法,这样类数组也可以使用。不过由于没有从 Array.prototype 继承,需要用 Function.call() 方法调用:

let likeArray = { "0": "a", "1": "b", "2": "c", length: 3 }
Array.prototype.join.call(likeArray, "+") // => "a+b+c"
Array.from(likeArray) // => (3) ["a", "b", "c"]

作为数组的字符串

JS 字符串的行为类似 UTF-16 Unicode 字符的只读数组,除了用 charAt() 方法,也可以用方括号语法:

let s = "test"
s.charAt(0) // => "t"
s[1] // => "e"
Array.prototype.join.call(s, " ") // => "t e s t"

要记住的是,字符串是不可修改的值,在把它们当成数组使用的时候,不能对自己修改。比如像 push() 这样的方法是不会有效果的(但不会抛出错误)。

参考

Array - MDN

展开语法 - MDN