数组
数组 是值的有序集合,其中的值称为元素,每个元素有一个数值表示的位置称为索引。
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()
会返回 undefined
,findIndex()
返回 -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()
这样的方法是不会有效果的(但不会抛出错误)。