js 的 Array

这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战

前言

数组是我们经常使用的数据类型。相信经常使用的 push() 和 索引取值感觉再熟悉不过了,但是可能会对 flat() 之类的比较陌生

作为前端知识大树的基石之一,不管从深度还是广度上我们都需要对其进行掌握

1629168520454.png

数组的定义

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 的两种方法去重

总结

  1. 将 Array 的方法分类记忆

  2. es6 数组的方法大大的提高了操作数组的能力,但如果要考虑到兼容性的时候需要 babel 或者使用 es5 用法替换

  3. 数组是引用数据类型,需要注意本身的拷贝和复制。数组的成员可以是任意类型,如果是引用类型也需要注意

  4. 在对数组遍历操作的方法通常会有一个回调函数和 this 指向两个入参,其中回调函数又有 当前成员、当前索引和原数组三个入参。需要注意回调函数使用箭头函数写法的时候 this 会指向 window 对象

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享