常用数组方法整理
最近写项目总是会处理一些很多的数据,而后端传过来的数据基本上都是数组的类型,所有避免不了我们去对这些数据进行处理,这里我列举了一些日常里常用的数组方法。
首先我们先来了解一下数组的一些常用方法
concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined
findIndex() 方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。
indexOf() 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。 (通常用它判断数组中有没有这个元素)
includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。
join() 方法将一个数组(或一个 类数组对象 ) 的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个项目,那么将返回该项目而不使用分隔符。
pop() 方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。
push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。
shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。
unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组
splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
reverse() 方法将数组中元素的位置颠倒,并返回该数组。该方法会改变原数组。
sort() 方法用 原地算法 对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的
常用的数组循环方法
forEach() 方法对数组的每个元素执行一次给定的函数
map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
reduce() 方法对数组中的每个元素执行一个由您提供的reducer 函数(升序执行),将其结果汇总为单个返回值。
every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。
filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
some() 方法测试是否至少有一个元素可以通过被提供的函数方法。该方法返回一个 Boolean 类型的值。
以上所有注解均来自 MDN
先来对常用的数组循环方法进行解刨
1、Array.forEach()
forEach() 方法对数组的每个元素执行一次给定的函数。
参数:forEach()接收两个参数
参数1:callback 为数组中每个元素执行的函数,该函数接收一至三个参数:
item 数组中正在处理的当前元素
index 数组中正在处理的当前元素的索引
array forEach() 方法正在操作的数组
参数2:thisArg 可选
可选参数。当执行回调函数 callback 时,用作 this 的值。
返回值:undefined
问:forEach() 能直接修改原数组?
场景1
const arr = [1, 2, 3]
arr.forEach(item => {
item = item + 1
console.log(item);
});
console.log(arr);
可以看到并没有修改原数组的元素
场景2
const arr = [1, 2, 3]
arr.forEach((item, index) => {
arr[index] = item + 1
console.log(item);
});
console.log(arr);
回到问题:
可以看到forEach() 虽然无法直接修改原数组的元素,但forEach() 可以间接修改原数组的值,所以这个问题是个坑
面试回答:forEach 不会直接改变调用它的对象,但是那个对象可能会被 callback 函数改变。
问:forEach() 能否修改内部 this 的指向
场景1
function Counter() {
this.sum = 0;
this.count = 0;
}
Counter.prototype.add = function (array) {
array.forEach(function (entry) {
console.log(this);
})
}
const obj = new Counter();
obj.add([1]);
此时可以看到打印的是 Window
场景2
function Counter() {
this.sum = 0;
this.count = 0;
}
Counter.prototype.add = function (array) {
array.forEach(function (entry) {
this.sum += entry;
++this.count;
console.log(this);
}, this);
};
const obj = new Counter();
obj.add([2, 5, 9]);
console.log(obj.count);
// 3 === (1 + 1 + 1)
console.log(obj.sum);
16 === (2 + 5 + 9)
这里我们在 forEach() 中添加第二个参数,打印的结果为 Counter
如果 thisArg 参数有值,则每次 callback 函数被调用时,this 都会指向 thisArg 参数。如果省略了 thisArg 参数,或者其值为 null 或 undefined,this 则指向全局对象。按照函数观察到 this 的常用规则,callback 函数最终可观察到 this 值。
2、Array.map()
map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
参数:map()接收两个参数
参数1:callback 为数组中每个元素执行的函数,该函数接收一至三个参数:
item 数组中正在处理的当前元素
index 数组中正在处理的当前元素的索引
array forEach() 方法正在操作的数组
参数2:thisArg 可选
可选参数。当执行回调函数 callback 时,用作 this 的值。
返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。
map() 的基本使用与 forEach 相同,只是 mpa 会返回一个新的数组
const arr = ["1", "2", "3"];
const newArr = arr.map(item => item = 'a')
console.log(arr);
console.log(newArr);
可以看到他并没有修改原数组,会返回一个新数组
如果map 可以返回一个新对象,那么我们不传任何值 mpa 会返回新数组吗?
const arr = ["1", "2", "3"];
console.log(arr.amp());
复制代码
来看打印
image.png
可以看到 map 的第一个参数的必传的,并且必须是函数
接下来我们来简单的实现一个map,来看看为什么 它可以返回一个新数组,而forEach不能
先来看 map
const Arr = ["1", "2", "3"];
// 在数组原型上挂载一个方法, Myap
Array.prototype.Myap = function (fn) {
// 先判断参数是否为 function
if (typeof fn !== "function") {
// 如果不是 function 则抛出一个 错误信息
throw new TypeError(`${fn} is not a function`);
}
// 创建一个空数组
let newArr = [];
// 利用 for 循环, 加上数组中的 push
// psuh 将传入的回调函数(fn)执行的返回值追加到空数组中
for (let i = 0; i < this.length; i++) {
// 将 this 中的每一项 当做实参, 传入 fn 中
// 这里的 this 指向的是 arr
// newArr.push(fn(this[i], i, this))
newArr.push(fn(this[i], i, this))
// 第一个参数 this[i] 也就是 数组的每一项
// 第二个参数 i 也就是数组的 索引
// 第三个参数 this 指的就是 原数组
};
// 之后返回 数组
return newArr;
}
复制代码
测试结果
const MyapArr = Arr.Myap((item, index, arr) => {
console.log(item, index, arr);
return item
})
console.log('Myap', MyapArr);
console.log('---------------------');
const mapArr = Arr.map((item, index, arr) => {
console.log(item, index, arr);
return item
})
console.log('map', mapArr);
我们来看 forEach
const Arr = ["1", "2", "3"];
Array.prototype.for_Each = function (fn) {
for (var i = 0; i < this.length; i++) {
fn(this[i], i, this)
}
}
console.log(Arr.for_Each((item, i, arr) => {
item = 'a'
console.log(item, i, arr);
}));
复制代码
测试结果
console.log(Arr.for_Each((item, i, arr) => {
item = 'a'
console.log(item, i, arr);
}));
console.log('for_Each', Arr);
console.log('---------------------');
console.log(Arr.forEach((item, i, arr) => {
item = 'a'
console.log(item, i, arr);
}));
console.log('forEach', Arr);
是不是与原方法完全一样。不过我们实现的也只是第一个参数,第二个参数感兴趣的可以去了解了解
3、Array.reduce()
reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
我们先来看 reduce() 方法的基本使用
const array = [1, 2, 3, 4];
const reducer = (totalizer, item, index, arr) => totalizer + item;
// 1 + 2 + 3 + 4
console.log(array.reduce(reducer));
// 5 + 1 + 2 + 3 + 4
console.log(array.reduce(reducer, 5));
复制代码
参数1:
callback 执行数组中每个值 (如果没有提供 totalizer 则第一个值除外)的函数,包含四个参数:
totalizer 累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或 totalizer。
item 数组中正在处理的元素。
index 数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则从索引1起始。
arr 调用reduce()的数组
参数2:
totalizer 作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。
我们之直接上源码分析
// 在数组的原型上挂载一个 MyReduce 方法 接收两个形参
Array.prototype.MyReduce = function (reducer, initVal) {
// 先判断 第一个 参数是否为 function
if (typeof reducer !== "function") {
// 如果不是 function 则抛出一个 错误信息
throw new TypeError(`${reducer} is not a function`);
}
// 利用循环调用 reducer 并将返回值赋值给 initVal
// 这样也就不难明白为啥, 需要一个累计器, 以及在 reducer 一定要有返回值
for (let i = 0; i < this.length; i++) {
initVal = reducer(initVal, this[i], i, this);
// 这里的四个参数分别为
// 第一个参数 initVal 累计器
// 第二个参数 this[i] 也就是 数组的每一项
// 第三个参数 i 也就是数组的 索引
// 第四个参数 this 指的就是 原数组
}
// 最后返回 累计器 initVal
return initVal
};
复制代码
测试结果
array.MyReduce((acc, item, index, arr) => {
console.log(acc, item, index, arr);
return acc + item
}, 0)
console.log('---------------');
array.reduce((acc, item, index, arr) => {
console.log(acc, item, index, arr);
return acc + item
}, 0)
这里总结一句
对于所有的循环方法的都 不会直接改变调用它的对象,但是那个对象可能会被 callback 函数改变。 every() 、filter() 、some() 这三个方法实现的逻辑很相似,与上面的三种方法原理差不多,感兴趣的可以的 MDN 了解了解
这里我们来对常用数组方法进行解刨
1、Array.concat()
01、 我们知道源数组调用 concat , 传入一个数组或者多个 它可以合并数组那,我们不传值或者 传入非数组类型的数据 Object、 undefined 、null … 会是什么?
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
console.log(array1.concat());
const obj = { a: '对象' }
console.log(array1.concat(obj, undefined, null, 1, '2', () => { }));
可以看到 当我们不传入任何值 会返回原数组 , 当传入非数组类型的数据 会作为数组的元素追加到原数组中。
02、concat() 的返回值 还是刚刚是数组我们来做测试
const arr = array1.concat()
arr[0] = 1
console.log(arr);
console.log(array1.concat());
这里我们可以看到 concat 会返回一个新数组 并且不会对原数组造成影响
2、Array.find()
01、 它可以返回数组中满足条件的第一个元素,如果 都不满足则返回 undefined 这里就不多概述了,我们来看看他的参数,首先它接收一个实参,该实参为 函数 ,函数中有三个形参,分别为 当前遍历到的元素、当前遍历到的索引、数组本身
const array = ['a', 'b', 'a'];
array.find((item, index, arr) => {
console.log(item, index, arr);
})
方法示例
const array = ['a', 'b', 'a'];
const res = array.find(item => item === 'a')
console.log(res);
测试
const array = ['a', 'b', 'a', 'b'];
const res = array.find(item => item = 'b')
console.log(res);
console.log(array);
这里进行赋值操作他会返回数组的第一个值?(大神求解)
这里应该可以判断出该方法不会直接修改原数组值
02、 那么他的参数是必传的吗?如果该函数返回一个 undefined 会是什么?
const array = ['a', 'b', 'a'];
console.log(array.find(() => { }));
console.log(array.find());
可以看到该参数为必传不传则会报错,如果该函数返回一个 undefined 则结果也为 undefined
3、Array.findIndex()
01、 该方法与 上一个 find 有类似的效果,它会返回数组中满足条件的第一个元素的索引,否则返回 -1,该方法的参数与上一个一样这里不在概述。
这里我们直接来测试
const array = ['a', 'b', 'a', 'b', 'c'];
const res1 = array.findIndex(item => item === 'c')
const res2 = array.findIndex(item => item === 'd')
console.log(res1);
console.log(res2);
console.log(array);
正常输出没有问题
来测试赋值操作
const array = ['a', 'b', 'a', 'b', 'c'];
const res1 = array.findIndex(item => item = 'c')
console.log(res1);
console.log(array);
可以看到它会返回第一个元素索引,也不能直接修改原数组的值
4、Array.includes()
01、 该方法有两个参数
第一个参数 :需要查找的元素值 查看数组中是否包含该参数,如果有返回 true 否则 false,
第二个参数 :为你要从第几个元素查找的索引值 , 如果传入一个负数、则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。
方法示例
const array = ['a', 'b', 'a', 'b', 'c'];
const res1 = array.includes('c', -1)
console.log(res1);
console.log(array);
我们来看另一种情况
const array = ['a', 'b', 'a', 'b', 'c'];
const res1 = array.includes('b', -1)
console.log(res1);
console.log(array);
打印居然为 falss ???
不慌我们来捋一捋
我们先想一想 第一次查找的是 c ,传入的索引为 -1 ,那问题来了我们是从第几个开始查找的呢?
在上面介绍过,如果传入的是一个负数,则按升序从 array.length + fromIndex 的索引开始搜
回到问题:数组 array 的 length 为 5,加上 -1 也就是从 索引为 4 的开始查找
正好 array[4] === c,而 在 c 的后面已经没有值了,所以 b 就找不到了
所以会返回 false
我们来测试一下
const array = ['a', 'b', 'a', 'b', 'c', 'b'];
const res1 = array.includes('b', -1)
console.log(res1);
console.log(array);
可以看到我们的理论是正确的
5、Array.indexOf()
01、 该方法与 findIndex 有些许类似,findIndex 是返回第一个满足条件的的元素,接收的一个函数。indexOf 是返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1
方法示例
const array = ['a', 'b', 'a', 'b', 'c'];
const res1 = array.indexOf('a', -1)
console.log(res1);
console.log(array);
同样来看另一种情况
const array = ['a', 'b', 'a', 'b', 'c'];
const res1 = array.indexOf('b', -1)
console.log(res1);
console.log(array);
这里与 includes 的原理一样,我们之间看验证结果
const array = ['a', 'b', 'a', 'b', 'c', 'b'];
const res1 = array.indexOf('b', -1)
console.log(res1);
console.log(array);
6、Array.join()
01、 简单的说他可以将数组以某个字符拼接成字符串。该方法如果不传入参数则会以逗号拼接 如果数组只有一个项目,那么将返回该项目而不使用分隔符
方法示例
const array = ['a', 'b', 'a', 'b', 'c', 'b'];
const res1 = array.join('=')
console.log(res1);
console.log(array);
02、 如果数组中有复杂数据类型
const array = ['a', 'b', 'a', 'b', 'c', 'b', {}];
const res1 = array.join('=')
console.log(res1);
console.log(array);
可以看到该参数是复杂数据类型是无法分隔的
7、Array.pop()
01、 方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。
该方法会直接修改原数组
方法示例
const array = ['a', 'b', 'c'];
const res1 = array.pop()
console.log(res1);
console.log(array);
02、 同样来看另一种场景
const array = ['a', 'b', 'c'];
const obj = {
'0': '1',
'1': '2',
'2': '3',
length: 3
}
console.log(array.pop.call(obj));
console.log(obj);
console.log(array);
MDN 的描述
pop 方法从一个数组中删除并返回最后一个元素。
pop 方法有意具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。pop方法根据 length属性来确定最后一个元素的位置。如果不包含length属性或length属性不能被转成一个数值,会将length置为0,并返回undefined。
如果你在一个空数组上调用 pop(),它返回 undefined
回到问题:
首先 pop 是 Array 原型上的方法, Object 类型的是无法调用的,cal() 方法是 Function 原型上的方法,array.pop.call(obj) 这里就是 数组调用 pop ,pop 调用 call() ,这里通过 call 改变了 pop 的 this 指向,将 pop 的 this 指向了 obj 。
8、Array.shift()
01、 shift 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。
从数组中删除的元素; 如果数组为空则返回 undefined
该方法会直接修改原数组
方法示例
const array = ['a', 'b', 'c'];
const res1 = array.shift()
console.log(res1);
console.log(array);
02、 同样来看另一种场景
const array = ['a', 'b', 'c'];
const obj = {
'0': '1',
'1': '2',
'2': '3',
length: 3
}
const res1 = array.shift.call(obj)
console.log(res1);
console.log(obj);
console.log(array);
MDN 的描述
shift 方法并不局限于数组:这个方法能够通过 call 或 apply 方法作用于类似数组的对象上。但是对于没有 length 属性(从0开始的一系列连续的数字属性的最后一个)的对象,调用该方法可能没有任何意义。
原理与 pop 一样
9、Array.push()
01、 push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。
该方法会直接修改原数组
方法示例
const array = ['a', 'b', 'c'];
const res1 = array.push('d', 'e')
console.log(res1);
console.log(array);
02、 同样来看另一种场景
const obj = {
length: 0,
addElem: function addElem(elem) {
// obj.length 自动递增
// 每次添加一个元素。
[].push.call(this, elem);
}
};
// 让我们添加一些空对象来说明。
obj.addElem('a');
obj.addElem({});
obj.addElem({});
console.log(obj.length);
console.log(obj);
MDN 的描述
push 方法具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。
实现的原理与 pop 类似,这里我们可以看到他给对象obj添加了一个属性方法addElem,该方法通过数组身上的push方法通过call来调用改变 push 内部的 this 指向了该对象obj的属性方法addElem身上。所以当我们调用 obj.addElem() 向里面传值时,也就是调用了push函数内部的代码。
10、Array.unshift()
01、 unshift 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组 ) 。
该方法会直接修改原数组
方法示例
const array = ['a', 'b', 'c'];
const res = array.unshift('1', '2')
console.log(res);
console.log(array);
02、 同样来看另一种场景
这里我通过 [].unshift.call() 来直接调用,并没有使用 push 方法描述中的把方法挂载到对象身上,其实原理都是一样的
const obj = {
'0': '1',
'1': '2',
'2': '3',
length: 3
}
const res1 = [].unshift.call(obj, 'a', 'b', {})
console.log(res1);
console.log(obj);
MDN 的描述
unshift 特意被设计成具有通用性;这个方法能够通过 call 或 apply 方法作用于类数组对象上。不过对于没有 length 属性(代表从0开始的一系列连续的数字属性的最后一个)的对象,调用该方法可能没有任何意义。
从这我们可以得出 unshift 与 push 实现的原理是一样的
11、Array.splice()
01、 splice 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
该方法会直接修改原数组
该方法接受三个参数:
start指定修改的开始位置
指定修改的开始位置(从0计数)。如果超出了数组的长度,则从数组末尾开始添加内容;如果是负值,则表示从数组末位开始的第几位(从-1计数,这意味着-n是倒数第n个元素并且等价于array.length-n);如果负数的绝对值大于数组的长度,则表示开始位置为第0位。
deleteCount整数,表示要移除的数组元素的个数
如果 deleteCount 大于 start 之后的元素的总数,则从 start 后面的元素都将被删除(含第 start 位)。
如果 deleteCount 被省略了,或者它的值大于等于array.length - start(也就是说,如果它大于或者等于start之后的所有元素的数量),那么start之后数组的所有元素都会被删除。
如果 deleteCount 是 0 或者负数,则不移除元素。这种情况下,至少应添加一个新元素。
item1, item2, …
要添加进数组的元素,从start 位置开始。如果不指定,则 splice() 将只删除数组元素
返回值:
由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。
代码实例
const array = ['a', 'b', 'c'];
const res = array.splice(0, 2)
console.log(res);
console.log(array);
02、 同样来看另一种场景
场景1
const array = ['a', 'b', 'c'];
const res = array.splice(0, 2, 'd')
console.log(res);
console.log(array);
这里首先是把原数索引为 0 开始删除了 2 个,然后在原数组的第 0 项添加了 d
场景2
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const res1 = array1.splice(-1, 2, '新增的1')
const res2 = array2.splice(-10, 2, '新增的2')
console.log(res1);
console.log(array1);
console.log('--------------------');
console.log(res2);
console.log(array2);
当指定修改位置为 负数时候,则表示从数组末位开始的第几位(也就是倒数第一位),所以会从最后一个 ‘c’ 开始删除 ,删除 2个 ,应为 c 已经是最后一个元素了 所以只删除了一个,之后在倒数第一位的位置添加了 一个新元素 新增的1
这里可以看到,当前指定修改的位置的值为 -10 ,根据 res1 打印的理论那么应该从 倒数第 10 为开始删除,但原数组的长度只有 3 个 ,所以这里就直接从第 0 个开始删除了 ,同样在第 0 位添加了一个元素 新增的2
12、Array.reveres()
reveres() 方法将数组中元素的位置颠倒,并返回改数组,数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个。该方法会改变原数组。
该方法会直接修改原数组
方法示例
const array = [1, 2, 3, 4, 5]
const res = array.reverse()
console.log(res);
console.log(array);
返回值:返回翻转后的数组