Skip to content

数组

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. 数组长度

数组有两个特殊行为:

  1. 如果给索引 i 的元素赋值,且 i 大于等于 length,则数组的 length 属性会被设置为 i+1。
  2. 如果将数组的 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() 返回 undefinedfindIndex() 返回 -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() 是数组的断言方法,对数组元素调用传入的断言函数,最终返回 truefalse
  • 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() 用于检查数组内是否存在某元素,返回 truefalse
  • 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,因此不能直接使用数组方法。