这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战
前言
数组是我们经常使用的数据类型。相信经常使用的 push() 和 索引取值感觉再熟悉不过了,但是可能会对 flat() 之类的比较陌生。
作为前端知识大树的基石之一,不管从深度还是广度上我们都需要对其进行掌握
数组的定义
Array 是 JavaScript 的原生对象,可以作为构造函数创建新数组。Array的原型上有很多方法让我们对数组进行操作,数组实例继承了这些方法
数组是引用数据类型。数据长度不固定,存储在堆中,变量只是对数据的引用。
创建数组
数组字面量
const arr1 = []
复制代码
Array.of() & new Array()
Array.of() 将一组值转化为数组
new Array() 创建数组
Array.of(item1, item2, ..., itemn)
复制代码
- item1, item2, …, itemn: 选填,将会转化为数组的一组值
const arr1 = new Array()
const arr2 = new Array(1)
const arr3 = new Array(1, 2)
const arr4 = Array.of()
const arr5 = Array.of(1)
const arr6 = Array.of(1, 2)
console.log(arr1) // []
console.log(arr2) // [ ,]
console.log(arr3) // [1, 2]
console.log(arr4) // []
console.log(arr5) // [1]
console.log(arr6) // [1, 2]
复制代码
Array.of() 和 new Array() 都是创建数组的方法,区别在于 new Array() 处理一个入参的时候创建入参长度的数组。而 Array.of() 入参数量多少都保持创建行为的一致性
Array.from()
Array.from() 将类数组对象和可遍历对象转化为数组
arr = Array.from(obj, (x) => { return x++ }, funThis)
复制代码
- obj: 必填,类数组对象或可遍历对象
- () => {}: 可选,遍历的回调,类似 map 作用
- funThis: 可选,遍历函数的 this
const obj1 = {
0: 'a',
'1': 'b',
5: 'c',
length: 3
}
const arr1 = Array.from(obj1)
console.log(arr1) // ['a', 'b', undefined]
复制代码
上面为类数组对象使用 Array.of() 转化为数组的过程。其中 length 为必要属性,决定数组的长度。对象的键名生成索引
const set1 = new Set([1, 2])
const arr1 = Array.from(set1)
console.log(arr1) // [1, 2]
console.log([...set1]) // [1, 2]
复制代码
将可迭代对象转化为数组,同时可迭代对象也可以使用扩展运算符转化
第二个 map 的回调函数
const set1 = new Set([1, 2])
const arr1 = Array.from(set1, () => {
return ++x
})
console.log(arr1) // [2, 3]
复制代码
arr1.concat()
concat() 可以连接多个数组,返回连接的数组,不改变原数组
arr1 = arr2.concat(arr3, arr4)
复制代码
- arr2: 在 arr2 的基础上添加数据
- arr3 arr4: 数组,填充到 arr2
const arr1 = [1, 2, {a: 3}]
const arr2 = [4, 5, 6]
const arr3 = [7, 8, 9]
const arr4 = arr1.concat(arr2, arr3)
arr1[0] = 0
arr1[2].a = 33
console.log(arr1) // [0, 2, {a: 33}]
console.log(arr4) // [1, 2, {a: 33}, 4, 5, 6, 7, 8, 9]
复制代码
修改 arr1 中的对象元素的时候,会影响到拷贝的新数组。修改基础类型数据不会。concat() 是数组的浅拷贝的一种
arr.slice()
slice() 从已有数组中返回选定的元素组成的数组
arr.slice(start, end)
复制代码
- start: 选填,从 start 索引开始,负值从尾部开始计算
- end: 选填,从 end 索引结束,不包括 end。如果没有填写则从 start 到最后
const arr1 = ['a', 'b', 'c']
const arr2 = ['a', 'b', 'c']
arr1.slice(1)
arr2.slice(-1, 0)
console.log(arr1) // ['b', 'c']
console.log(arr2) // []
复制代码
slice() 从数组中截取一段生成数组来用
读取数组
索引读取
const arr1 = ['a', 'b', 'c']
const res1 = arr1[0]
const res2 = arr1[0.0]
const res3 = arr1['0']
const res4 = arr1['0.0']
console.log(res1) // 'a'
console.log(res2) // 'a'
console.log(res3) // 'a'
console.log(res4) // undefined
复制代码
中括号中的值会被转换为数值,但是 ‘0.0’ 这种就不能被转化。类似对象属性的访问
arr.at()
获取数组元素,可以传入负索引,弥补了中括号不能读取负值
const arr1 = ['a', 'b', 'c']
const res1 = arr1.at(-1)
const res2 = arr1.at(1)
const res3 = arr1.at(false)
const res4 = arr1.at(true)
const res5 = arr1.at('')
console.log(res1) // 'c'
console.log(res2) // 'a'
console.log(res3) // 'a'
console.log(res4) // 'b'
console.log(res5) // 'a'
复制代码
如果传入的数据不为整数。判断是否为 true,实则取索引1的值,否则取索引0的值
这个属性需要注意兼容性,chrome 到 92 才支持
? 这里有一个疑问了,如果at()主要是读取负索引值,那为啥不直接修改 arr[] ?我想是为了不破坏原来 arr[] 读取负数的逻辑,arr[-1]读取的 -1 为键名的值
修改数组
arr.splice()
添加或删除数组中的元素,会修改原数组。返回被删除的数组
arr.splice(index, num, item1, item2, ..., itemn)
复制代码
- index: 必填,索引 index 位置添加/删除数组
- num: 选填,表示删除的数量,默认为数组结尾
- item1, item2, …, itemn: 选填,添加到索引的数据
const arr1 = ['a', 'b', 'c', 'd']
const arr2 = ['a', 'b', 'c', 'd']
const arr3 = ['a', 'b', 'c', 'd']
const arr4 = ['a', 'b', 'c', 'd']
const arr5 = arr1.splice(2, 0)
const arr6 = arr2.splice(2, 1)
const arr7 = arr3.splice(2)
const arr8 = arr4.splice(2, 0, 'add')
console.log(arr1) // ['a', 'b', 'c', 'd']
console.log(arr2) // ['a', 'b', 'd']
console.log(arr3) // ['a', 'b']
console.log(arr4) // ['a', 'b', 'add', 'c', 'd']
console.log(arr5) // []
console.log(arr6) // ['c']
console.log(arr7) // ['c', 'd']
console.log(arr8) // []
复制代码
splice() 满足了我对数组的添加和删除的要求。对比其他数组的操作方法也可以用 splice() 来改写,区别在于哪个更合适,即写法和效率上
arr.push() & arr.unshift()
push() 向数组末尾添加一个或多个数据,返回数组长度
unshift() 向数组开始位置添加一个或多个数据,返回数组长度
arr.push(item1, item2, ..., itemn)
arr.unshift(item1, item2, ..., itemn)
复制代码
- item1, item2, …, itemn: 必填,要添加数组的数据
const arr1 = ['a', 'b', 'c']
const arr2 = ['a', 'b', 'c']
const len1 = arr1.push('d', 'e')
const len2 = arr1.unshift('d', 'e')
console.log(arr1) // ['a', 'b', 'c', 'd', 'e']
console.log(arr2) // ['d', 'e', 'a', 'b', 'c']
console.log(len1) // 5
console.log(len2) // 5
复制代码
push() 和 unshift() 将一组数据添加到数组的开始或结尾,两个可以一起理解
splice() 版本
const arr1 = ['a', 'b', 'c']
const arr2 = ['a', 'b', 'c']
arr1.splice(arr1.length, 0, 'd', 'e')
arr2.splice(0, 0, 'd', e')
console.log(arr1) // ['a', 'b', 'c', 'd', 'e']
console.log(arr2) // ['d', 'e', 'a', 'b', 'c']
复制代码
arr.pop() & arr.shift()
pop() 删除数组的最后一个元素,返回删除的元素
shift() 删除数组第一个元素,返回删除的元素
arr.pop()
arr.shift()
复制代码
const arr1 = ['a', 'b', 'c']
const arr2 = ['a', 'b', 'c']
const item1 = arr1.pop()
const item2 = arr2.shift()
console.log(arr1) // ['a', 'b']
console.log(arr2) // ['b', 'c']
console.log(item1) // 'c'
console.log(item2) // 'a'
复制代码
删除数组首尾数据用这两个方法更加的简洁,其他好像没啥好处
splice() 版本
const arr1 = ['a', 'b', 'c']
const arr2 = ['a', 'b', 'c']
arr1.splice(arr1.length, 1)
arr2.splice(0, 1)
console.log(arr1) // ['a', 'b']
console.log(arr2) // ['b', 'c']
复制代码
arr.copyWithin()
指定一个位置开始替换数据,数据来自于自身的一段。此方法会修改数组自身,返回修改后的数组
arr.copyWithin(target, start, end)
复制代码
- target: 必填。从此位置开始替换,负值则倒数
- start: 选填。替换元素的开始索引,负值为倒数
- end: 选填。替换元素的结束索引,不包括自己,复制为倒数
const arr1 = ['a', 'b', 'c', 'd']
const arr2 = ['a', 'b', 'c', 'd']
arr1.copyWithin(1, 0, 1)
arr2.copyWithin(-1, 0, 1)
console.log(arr1) // ['a', 'a', 'c', 'd']
console.log(arr2) // ['a', 'b', 'c', 'a']
复制代码
可以用 splice() 做到相同的效果,但是在写法和语意上 copyWithin 比 splice() 更加合适。而且 splice() 能够使用被删除的元素作为添加的元素
const arr1 = ['a', 'b', 'c', 'd']
const arr2 = ['a', 'b', 'c', 'd']
const arr3 = ['a', 'b', 'c', 'd']
const arr4 = ['a', 'b', 'c', 'd']
arr1.copyWithin(1, 0, 1)
arr2.splice(1, 1, arr2[0])
arr3.copyWithin(-3, 0, 4)
arr4.splice(-3, 3, arr4[0], arr4[1], arr4[2])
console.log(arr1) // ['a', 'a', 'c', 'd']
console.log(arr2) // ['a', 'a', 'c', 'd']
console.log(arr3) // ['a', 'a', 'b', 'c']
console.log(arr4) // ['a', 'a', 'b', 'c']
复制代码
arr.sort()
sort() 对数组元素进行排序,改变原数组
arr.sort(() => {})
复制代码
- () => {}: 选填,但一定要是函数。排序的依据
const arr1 = [1, 3, 2]
const arr2 = ['a', 'c', 'b']
const arr3 = ['aa', 'ac', 'ab']
const arr4 = [1, 3, 2]
arr1.sort()
arr2.sort()
arr3.sort()
arr4.sort((a, b) => {
return a - b
})
console.log(arr1) // [1, 2, 3]
console.log(arr2) // ['a', 'b', 'c']
console.log(arr3) // ['aa', 'ab', 'ac']
复制代码
默认的排序规则是依据 Unicode 编码的大小排序,即0-9 => A-Z => a-z。并且按照升序的顺序排列,即从小到大
排序算法实际上是返回一个正负值。负值:排序不变;正值:排序交换。默认排序的可以理解成逐个字符的 Unicode 编码相减
排序稳定性:关键字相同的项目,排序前后的顺序不变。ES2019 规定 Array.prototype.sort() 的默认排序算法必须稳定
const arr1 = [
{
a: 1,
b: '1-1'
},
{
a: 3,
b: '3-1'
},
{
a: 2,
b: '2-1'
},
{
a: 3,
b: '3-2'
}
]
function sortFun1 (s1, s2) {
return s1.a < s2.a ? -1 : 1
}
arr1.sort(sortFun1)
console.log(JSON.stringify(arr1, null, 4))
[
{
"a": 1,
"b": "1-1"
},
{
"a": 2,
"b": "2-1"
},
{
"a": 3,
"b": "3-1"
},
{
"a": 3,
"b": "3-2"
}
]
复制代码
我们编写的排序规则的函数也尽量遵循排序稳定性
arr.reverse()
反转数组的顺序,会改变原数组。返回值为自身
arr.reverse()
复制代码
const arr1 = [1, 2, 3]
arr1.reverse()
console.log(arr1) // [3, 2, 1]
复制代码
reverse() 对于数组的反转操作十分便捷,有且仅有这个功能,所有需要用到的时候要想到哦
arr.fill()
fill() 使用给定的值填充数组
arr.fill(value, start, end)
复制代码
- value: 可选,给定的值。不填写会使用 undefined 填充
- start: 可选,填充的开始索引
- end: 可选,填充的结束索引
const arr1 = ['a', 'b', 'c']
const arr2 = new Array(3)
arr1.fill('d', 0, 1)
arr1.fill(4)
console.log(arr1) // ['d', 'b', 'c']
console.log(arr2) // [4, 4, 4]
复制代码
fill() 用于填充空数组的时候显得很快捷
arr.flat() & arr.flatMap()
多维数组的拉平需要 flat() 和 flatMap(),返回新数组,不改变原数组
const arr1 = [1, [2, [3, [4]]]]
const arr2 = arr1.flat()
const arr3 = arr1.flat(2)
const arr4 = arr1.flat(Infinity)
console.log(arr2) // [1, 2, [3, [4]]]
console.log(arr3) // [1, 2, 3, [4]]
console.log(arr4) // [1, 2, 3, 4]
复制代码
flat() 默认拉开一层嵌套数组。可以设置拉开的层数,或者设置成 Infinity,即拉开每一层
如果数组中有空位,flat() 会跳过空位
const arr1 = [1, [2, 3]]
const arr2 = arr1.flatMap((value, index, arr) => {
return ++value
}, funThis
console.log(arr2) // [2, 3, 4]
复制代码
flatMap() 只能拉开一层嵌套,但是比 flat() 多了遍历的功能。同时也能够指定函数中的 this 对象,注意箭头函数
查找
arr.indexOf() & arr.lastIndexOf()
indexOf() 检查 value 在数组中的索引,没有则返回 -1
res1 = arr.indexOf(value)
res2 = arr.lastIndexOf(value)
复制代码
- value: 给定的值
如果数据是累加类型的数组,可以使用 lastIndexOf() 提交查找效率
es5 中常用的查找数组中的元素,在 es6 普及之后会使用的比较少
arr.includes()
判断数组是否包含 value,返回一个布尔值
arr.includes(value, index)
复制代码
- value: 给定的值
- index: 搜索的起始位置,负值为倒数位置
const arr1 = ['a', 'b', 'c', NaN, null]
const res1 = arr1.includes('a')
const res2 = arr1.includes(NaN)
const res3 = arr1.includes(null)
console.log(res1) // true
console.log(res2) // true
console.log(res3) // true
console.log(NaN === NaN) // false
复制代码
includes() 对 NaN 的判断是允许相等的,相较于 indexOf() 不同
find() & findIndex()
- find(): 找出第一个符合条件的数组成员,没有则返回 undefined
- findIndex(): 找出第一个符合条件的数组成员的索引,没有则返回 -1
arr.find((value, index, arr) => {
console.log(this === window) // true
return value > 1
}, funThis)
复制代码
- value: 当前的值
- index: 当前的索引
- arr: 原数组
- funThis: 回调函数内的 this。这里回调函数使用箭头函数,故指向 window
const arr1 = [1, 2, 3]
const result1 = arr1.find((x, index, arr) => {
console.log(x, index, arr)
return x > 1
})
console.log(result1)
// 2
复制代码
当找到符合条件的数组成员之后就会中止遍历,返回结果
相较于之前的 indexOf(),find() 和 findIndex() 更具有语意化和可操作性
遍历
arr.filter()
filter() 返回满足条件的数组成员的数组,不会改变原数组
arr1 = arr2.filter(function(value, index, arr){}, funThis)
复制代码
- functio(value, index, arr){}: 用于判断数组成员是否满足条件
- funThis: 函数中的 this
const arr1 = [2, -1, 4]
const arr2 = arr1.filter((value) => {
return value > 0
})
console.log(arr2) // [2, 4]
复制代码
arr.forEach()
forEach() 遍历数组成员,执行回调函数。不改变原数组,但是引用类型修改改变
arr1.forEach(function (value, index, arr) {}, funThis)
复制代码
- function (value, index, arr) {}: 遍历数组成员的回调函数
- funThis: 可选,函数内的 this
const arr1 = [1, 2, 3]
const arr2 = [{ a: 1 }, { a: 2 }, { a: 3 }]
arr1.forEach((x) => { return ++x })
arr2.forEach((x) => { return ++x.a })
console.log(arr1) // [1, 2, 3]
console.log(arr2) // [{ a: 2 }, { a: 3 }, { a: 4 }]
复制代码
arr.map()
map() 用于遍历数组,返回调整之后的元素组成的数组。不会改变原数组,不适用于引用类型
arr.map(function(value, index, arr) {}, funThis)
复制代码
- function (value, index, arr) {}: 必须,调整数组元素的回调
- funThis: 可选,函数内的 this
const arr1 = [1, -1, 3]
const arr2 = arr1.map((value) => {
return Math.abs(value)
})
console.log(arr1) // [1, -1, 3]
console.log(arr2) // [1, 1, 3]
复制代码
arr.every()
对数组每个元素进行检测,判断是否满足条件。返回布尔值
arr.every(function (value, index, arr) {
}, funThis)
复制代码
- function (value, index, arr) { return }: 必须,判断函数
- funThis: 可选,函数内的 this
const arr1 = [2, 4, 5]
const res1 = arr1.every((value) => {
return value > 0
})
console.log(res1) // true
复制代码
判断 arr1 中是否所有数据都大于0
arr.some()
检测数组中的元素是否满足于给定的条件,返回布尔值
arr.some(function(value, index, arr) {}, funThis)
复制代码
- function(value, index, arr) {}: 检测数组的条件
- funThis: 函数的 this
const arr1 = [1, -1, 3]
const res1 = arr1.some((value) => {
return value < 0
})
复制代码
只要有满足条件的情况,返回 true ,便不会继续遍历下去
arr.reduce() & arr.reduceRight()
reduce() 将数组的每个元素进行从左到右累计处理,返回最终值
res1 = arr.reduce((total, num) => {}, initValue)
复制代码
- (total, num) => {}: 对数组成员进行累计处理
- initValue: 可选,传递给函数的初始值,没有则使用第一个数组元素
const arr1 = [1, 2, 3]
const res1 = arr1.reduce((total, num) = {
return total * num
})
console.log(res1) // 6
复制代码
数组 arr1 经历了 1 * 2 * 3 操作,得到我们最终值 6
reduceRight(),类似 reduce(),不过顺序是从右到左
arr.keys() arr.values() arr.entries()
keys() values() entries() 都返回一个遍历器对象,可以用 for…of 遍历
- keys(): 对键名进行遍历,即索引
- values(): 对键值进行遍历
- entries(): 对键值对进行遍历,数组格式的键值对。可用数组的解构赋值
const arr1 = ['a', 'b', 'c']
for (const key of arr1.keys()) {
console.log(key)
}
// 0 1 2
for (const value of arr1.values()) {
console.log(value)
}
// 'a' 'b' 'c'
for (const [key, value] of arr1) {
console.log(key, value)
}
// 0 'a' 1 'b' 2 'c'
for(const item of arr1) {
console.log(item)
}
// 'a' 'b' 'c'
复制代码
数组是可遍历对象,除了上面方法的遍历还可以用 for…of 直接遍历。数组的直接遍历等价于 values()
其他
… 扩展运算符
将可迭代对象转化为逗号分隔的序列
const arr1 = [1, 2, 3]
const set1 = new Set([arr1])
console.log(...arr1) // 1 2 3
console.log(...set1) // 1 2 3
复制代码
… 用于数组的浅拷贝
const arr1 = [1, { a: 1 }]
const arr2 = [...arr1]
arr1[0] = 2
arr1[0].a = 2
console.log(arr2) // [1, { a: 2 }]
复制代码
字符串的转化。可以正确的识别四个字节的 Unicode 字符
const str1 = '我真帅?'
const len = str1.length
const arr1 = [...str1]
console.log(len) // 5
console.log(arr1) // ['我', '真', '帅', '?']
复制代码
arr.toString()
将数组转换为字符串,元素之间使用逗号连接。可以看成是特殊的 join()
arr.join()
join() 把数中所有的元素用入参拼接成字符串,
const arr1 = [1, 2, 3]
const arr2 = [{}, new Set()]
const arr3 = [1, [2, 3], [4, [5, 6]]]
const str1 = arr1.join('-')
const str2 = arr2.join('-')
const str3 = arr3.join('-')
const str4 = arr3.join(',')
console.log(str1) // '1,2,3'
console.log(str2) // '[object Object]-[object Set]'
console.log(str3) // '1-2,3-4,5,6'
console.log(str4.split(',')) // ['1', '2', '3', '4', '5', '6']
复制代码
上面最后一个例子可以联想到 es6 的 flat(),但是这里的元素都变成了字符串。数组的 join() 对于多维数组的处理方式是整个拉平
Array.isArray()
判断数据是否是数组类型,返回布尔值
const arr1 = []
const obj1 = {}
const res1 = Array.isArray(arr1)
const res2 = Array.isArray(arr2)
console.log(res1) // true
console.log(res2) // false
复制代码
特殊demo
async await 遍历数组
我想按照顺序执行一组异步任务,即上一个请求调用完毕之后调用另一个。
const arr1 = ['a', 'b', 'c']
function fun1() {
arr1.forEach(async(value) => {
const res1 = await api()
console.log(res1)
})
}
fun1()
async function fun2() {
for(const value of arr1) {
const res1 = await api()
console.log(res1)
}
}
function api () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 3000)
})
}
复制代码
fun1 会同时触发三个异步事件,fun2 依次执行三个异步事件。因为 forEach() 会同步将数组每个成员调用回调函数,for…of 则是依次执行
数组去重
const arr1 = ['a', 'b', 'c', 'a']
const arr2 = arr1.filter((value, key) => {
return arr1.indexOf(value) === key
})
const arr3 = [...new Set(arr1)]
console.log(arr2) // ['a', 'b', 'c']
console.log(arr3) // ['a', 'b', 'c']
复制代码
上面的两种方法是典型的 es5 和 es6 的两种方法去重
总结
-
将 Array 的方法分类记忆
-
es6 数组的方法大大的提高了操作数组的能力,但如果要考虑到兼容性的时候需要 babel 或者使用 es5 用法替换
-
数组是引用数据类型,需要注意本身的拷贝和复制。数组的成员可以是任意类型,如果是引用类型也需要注意
-
在对数组遍历操作的方法通常会有一个回调函数和 this 指向两个入参,其中回调函数又有 当前成员、当前索引和原数组三个入参。需要注意回调函数使用箭头函数写法的时候 this 会指向 window 对象