数组
1. 创建数组
创建数组有几种方式:
- 数组字面量。
- 对可迭代对象使用
...
拓展操作符。 Array()
构造函数。- 工厂方法
Array.of()
和Array.from()
。
1.1 数组字面量
数组字面量就是一对方括号中用逗号分隔的数组元素的列表:
javascript
let empty = [];
let numbers = [1, 2, 3];
数组元素可以是任意表达式,如对象字面量,数组字面量:
javascript
let b = [[1, { x: 1, y: 2}], [2, { x: 3, y: 4}]]
数组字面量中出现多个连续逗号且之间没值,则该数组是稀疏数组:
javascript
let count = [1, , 3] // 长度为3
let undefs = [,,] // 数组允许末尾出现逗号,因此长度为2
1.2 拓展操作符
ES6 之后,可以用拓展操作符 ...
在一个数组字面量中包含另一个数组的元素:
javascript
let a = [1, 2, 3]
let b = [0, ...a, 4] // [0, 1, 2, 3, 4]
拓展操作符是创建数组副本(浅拷贝)的一种方式:
javascript
let origin = [1, 2, 3]
let copy = [...origin]
copy[0] = 0;
origin[0] // 1, 浅拷贝
拓展操作符适用于任何可迭代对象(可迭代对象能用 for/of
遍历),如字符串是可迭代对象,可以用拓展操作符将字符串转为单个字符的数组:
javascript
let digits = [..."12345"]
digits // ['1', '2', '3', '4', '5']
特殊情况
在对象字面量中可以使用拓展操作符展开对象,此时会枚举对象的属性,并把键值对添加到新的对象。在对象字面量之外,如果对象不可迭代,则不能用拓展操作符展开。
javascript
let obj = { a: 1, b: 2 }
let copy = { ...obj } // 浅拷贝 copy == {a: 1, b: 2}
1.3 Array() 构造函数
- 不传参调用会创建一个空数组,等价于
[]
。 - 传入一个数值参数时可以指定数组长度,数组元素仍未定义:
javascript
let a = new Array(5);
- 传入一个非数值元素,或两个及以上参数,参数会成为新数组的元素:
javascript
let a = new Array(1, 2, 3)
a // [1, 2, 3]
1.4 Array.of()
工厂方法,使用其参数值作为数组元素并返回新数组,无论参数数量:
javascript
Array.of() // []
Array.of(5) // [5]
Array.of(1, 2, 3,) // [1, 2, 3]
1.5 Array.from()
- 工厂方法,期待一个可迭代对象或类数组对象,并返回包含该对象元素的新数组。
- 如果传入的是可迭代对象,则等价于使用拓展操作符创建数组。
- 类数组对象不是真的数组,但也有一个
length
属性,且每个属性的键都是整数。Array.from()
定义了一种给类数组创建真正数组副本的机制。 Array.from()
可以接受第二个参数,传入一个回调函数,在构建新数组时,会把源对象的元素传入回调函数,将返回值作为新数组的元素。
javascript
// 类数组
let obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
}
Array.from(obj) // ['a', 'b', 'c']
2. 读写数组元素
- 使用
[]
访问数组元素。 - 数组是一个特殊对象,数值索引会转为字符串,再作为属性名查找。
- 可以在数组上以任意名字创建属性,不过对于索引属性,数组会有特殊行为,即自动更新
length
属性,并且,数组遍历只会遍历索引属性。 - 数组索引其实就是一种特殊的对象属性,所以 JavaScript 数组没有越界错误,如果不存在则返回
undefined
。
javascript
let a = [1, 2, 3]
a['b'] = 1;
a // [1, 2, 3, b: 1]
for (let i = 0; i < a.length; i++) {
console.log(a[i]); // 1,2,3
}
a['10'] = 1
a.length // 11
3. 稀疏数组
- 稀疏数组的元素个数少于
length
属性的值。 - 数组的实现通常会进行特殊优化,从而让访问数组元素比常规对象属性更快,但对稀疏数组来说,访问元素与查询常规对象属性时间相当。
4. 数组长度
数组有两个特殊行为:
- 如果给索引 i 的元素赋值,且 i 大于等于
length
,则数组的length
属性会被设置为 i+1。 - 如果将数组的
length
属性设置为小于当前值的非负整数 n,则会将索引大于等于 n 的元素删除。
5. 添加和删除数组元素
push()
:在数组末尾添加一个或多个元素,添加一个元素等价于给length
索引位置赋值。unshift()
:在开头插入元素,会把已有元素移动到更高索引位。pop()
:删除数组最后一个元素并返回,数组长度减 1。shift()
:删除并返回数组第一个元素,数组长度减 1 并将所有元素移动到低一位的索引。splice()
:可以插入、删除或替换数组元素,会修改length
属性并移动数组元素。
javascript
let a = [];
a.push(1, 2)
a // [1, 2]
提示
可以使用 delete
删除数组元素,但不会修改 length
属性,也不会移动数组元素,删除后数组会变稀疏。
6. 迭代数组
for/of
循环是遍历数组元素最简单的方式,根据length
返回元素,不存在的返回undefined
。- 其它迭代方式包括常规
for
循环,forEach()
、map()
、filter()
等。
7. 数组方法
7.1 迭代方法
- 本节介绍的所有方法都不会修改原始数组。
- 这些方法都接受一个函数作为第一个参数,并对数组每个元素都调用一次这个函数。
- 多数方法接受可选的第二个参数,用于指定第一个函数参数的内部
this
值。
7.1.1 forEach()
forEach()
方法迭代数组每个元素,并调用传入的指定函数。forEach()
在调用回调函数时会传入三个参数:数组元素值、元素索引和数组本身。
javascript
let data = [1, 2, 3, 4, 5], sum = 0;
data.forEach((value) => {
sum += value;
}) // sum == 15
注意
forEach
并未提供一种提前终止迭代的方法。
7.1.2 map()
map()
方法把数组每个元素传给指定的函数,返回由该回调函数返回的元素构成的新数组。- 如果数组是稀疏的,则缺失元素不会调用回调函数,返回的数组与原数组长度相同,缺失元素也相同。
javascript
let a = [1, 2, 3];
a.map((x) => {
return x * x;
}) // [1, 4, 9]
7.1.3 filter()
filter()
方法会返回调用它的数组的子数组,如果回调函数返回true
或者返回值能转为true
,则该元素就是filter
最终返回的子数组的成员。
javascript
let a = [5, 4, 3, 2, 1]
a.filter(x => x < 3) // [2, 1]
注意
filter()
会跳过稀疏数组中缺失的元素,返回的数组始终是稠密的。
7.1.4 find() 与 findIndex()
find()
和findIndex()
方法会迭代元素,如果回调函数返回真值,则表明找到匹配元素,迭代终止,find()
返回匹配的元素,findIndex()
返回匹配元素的索引。- 如果没有找到匹配的元素,
find()
返回undefined
,findIndex()
返回 -1。
javascript
let a = [1, 2, 3, 4, 5];
a.findIndex(x => x === 3) // 2
a.findIndex(x => x < 0) // -1
a.find(x => x % 5 === 0) // 5
a.find(x => x % 7 === 0) // undefined
7.1.5 every() 与 some()
every()
与some()
是数组的断言方法,对数组元素调用传入的断言函数,最终返回true
或false
。every()
方法只在断言函数对数组所有元素都返回true
时才返回true
,否则返回false
。some()
方法只在断言函数对数组所有元素都返回false
才返回false
,否则返回true
。
javascript
let a = [1, 2, 3, 4, 5]
a.every(x => x < 10) // true
a.some(x => x % 2 === 0) // true
注意
every()
和 some()
会在知道返回什么后立即停止迭代数组。
7.1.6 reduce() 与 reduceRight()
reduce()
与reduceRight()
使用回调函数归并元素,并最终产生一个值。reduce()
接收两个参数,第一个参数是执行归并操作的函数,第二个参数是可选的,是传给归并函数的初始值。reduce()
方法的回调函数,第一个参数是目前为止归并操作的累积结果,第二、三、四个参数是当前元素值,索引和数组本身。reduce()
第一次被调用时,如果有初始值,则初始值传给回调函数的第一个参数,数组索引为 0 的元素传给第二个,如果不存在初始值,则数组第一、二个元素依次传给回调的第一、二个参数。reduceRight()
与reduce()
类似,只不过是从高索引向低索引(从右向左)处理数组。如果归并操作具有从右向左的结合性,可以考虑使用reduceRight()
。
javascript
let a = [2, 3, 4];
a.reduceRight((acc, val) => val ** acc) // 2 ** (3 ** 4)
7.2 flat() 与 flatMap() 打平数组
flat()
方法会创建一个新数组,原数组内的数组元素会打平作为新数组的元素。- 如不传参数,默认打平一级,可以传一个数值参数,表示打平层级。
flatMap()
方法与map()
方法类似,只不过返回的数组会自动打平,就像传给了flat
一样,调用a.flatMap(f)
跟a.map(f).flat()
效果一样,但前者效率更高。flatMap()
可以把输入数组中的一个元素映射为输出数组中的多个元素。特别地,可以把输入元素映射为空数组,打平后并不会有元素出现在输出数组中。
javascript
let a = [1, [2, [3, [4]]]];
a.flat(1) // [1, 2, [3, [4]]]
a.flat(2) // [1, 2, 3, [4]]
a.flat(3) // [1, 2, 3, 4]
a.flat(4) // [1, 2, 3, 4]
// 将非负数映射为平方根
[-2, -1, 1, 2].flatMap(x => x < 0 ? [] : Math.sqrt(x)) // [1, 2**0.5]
7.3 使用 concat() 添加数组
concat()
方法用于拼接数组,如果参数列表中有数组,则会打平数组再拼接,但不会递归打平。- 不会修改原始数组。
javascript
let a = [1, 2, 3];
a.concat(4, 5) // [1, 2, 3, 4, 5]
a.concat([4, 5], [6, 7]) [1, 2, 3, 4, 5, 6, 7]
a.concat(4, [5, [6, 7]]) [1, 2, 3, 4, 5, [6, 7]]
注意
concat()
方法会创建副本,成本较高,如果需要对原数组进行添加,应考虑使用 push()
或 splice()
就地修改。
7.4 数组实现栈和队列
- 通过
push()
和pop()
可以实现栈,通过shift()
和unshift()
也可以,但需要移动元素,效率不如前者。 push()
方法不会打平传入的数组,如想打平可以使用拓展运算符...
。- 使用
push()
在末尾添加元素,shift()
在头部删除元素,可以实现队列。
javascript
let q = []
q.push(1, 2); // q == [1, 2]
q.shift() // q == [2] 返回1
q.push(3) // q == [2, 3]
q.shift() // q == [3] 返回2
q.shift() // q == [] 返回2
7.5 slice()、splice() 和 fill()
slice()
返回数组的切片,接收两个参数,第一个参数指定开始位置,第二个参数可选,指定结束位置但不包括该位置。该方法不会修改原数组。splice()
是对数组进行插入和删除的方法,会修改调用它的数组。第一个参数指定要插入或删除的开始位置,第二个参数是要从数组中删除的元素个数,如果省略第二个参数,从开始位置的后面全部元素都会删除。
javascript
let a = [1, 2, 3, 4, 5, 6, 7, 8]
a.splice(4) // 返回[5, 6, 7, 8] a == [1, 2, 3, 4]
a.splice(1, 2) // 返回[2, 3] a == [1, 4]
a.splice(1, 1) // 返回[4] a == [1]
splice()
在前两个参数后还能跟任意多参数,表示在第一个参数位置插入到数组中的元素。
javascript
let a = [1, 2, 3, 4, 5]
a.splice(2, 0, "a", "b") // 返回[] a = [1, 2, "a", "b", 3, 4, 5]
a.splice(2, 2, [1, 2], 3) // 返回["a", "b"] a = [1, 2, [1, 2], 3, 3, 4, 5]
fill()
方法将数组或切片设置为指定值,第一个参数是要设置的值,第二三个参数可选,分别指定切片的起始位置。它会修改调用的数组,并返回修改后的数组。
javascript
let a = new Array(5);
a.fill(0) // [0, 0, 0, 0, 0]
a.fill(9, 1) // [0, 9, 9, 9, 9]
7.6 数组索引与排序方法
7.6.1 indexOf() 和 lastIndexOf()
indexOf()
和lastIndexOf()
搜索元素并返回第一个匹配的元素,如果没找到则返回 -1。indexOf()
从前往后搜索,lastIndexOf()
从后往前。indexOf()
和lastIndexOf()
使用===
操作符进行比较。- 二者都接受第二个可选参数,用于指定搜索开始的位置。
7.6.2 includes()
includes()
用于检查数组内是否存在某元素,返回true
或false
。includes()
用===
比较参数和数组元素,但对NaN
会特殊处理,认为NaN
与自身相等,这与indexOf()
不同。
javascript
let a = [1, true, 3, NaN];
a.includes(true) // true
a.includes(NaN) // true
a.indexOf(NaN) // -1
7.6.3 sort()
- 不传参时,
sort()
按字母顺序对元素进行排序,必要时会将元素转为字符串。 - 如果数组包含未定义的元素,会被排到数组末尾。
- 要执行非字母顺序排序,必须传入一个回调函数。该函数有两个参数,如果第一个参数必须排在第二个参数前面,则回调函数应该返回一个小于 0 的值,如果第一个参数排在后面,则应该返回大于 0 的值,如果相等,则应该返回 0。
javascript
let a = [33, 4, 1111, 22];
a.sort((left, right) => {
return left - right;
}) // [4, 22, 33, 1111]
7.6.4 reverse()
reverse()
可以反转数组元素顺序。- 不会创建新数组,会修改原数组。
javascript
let a = [3, 2, 1]
a.reverse() // a == [1, 2, 3]
7.7 数组到字符串的转换
join()
可以将数组元素转为字符串并进行拼接,可以传入一个字符串用于分隔数组元素,不传默认使用逗号,
:
javascript
let a = [1, 2, 3]
a.join() // "1,2,3"
a.join(" ") // "1 2 3"
a.join("") // "123"
- 数组的
toString()
方法效果与不传参的join()
方法一样。
7.8 静态数组函数
Array.isArray()
方法用于确定一个值是不是数组。
8. 类数组对象
- 只要一个对象有
length
属性和相应的非负整数属性,就是类数组对象。 - 由于没有继承
Array.prototype
,因此不能直接使用数组方法。