前言: 众所周知,JavaScript 中丰富的内置数组方法为数组操作赋予了极高的灵活性,让开发者能够更高效地处理数据。然而,这些五花八门的方法也时常让人在使用时感到眼花缭乱 ------ 比如,我们常会困惑:这个方法究竟该传入函数还是其他类型的参数?它的返回值是什么样的?调用后原数组会被修改,还是会保持原样?
为化解上述困惑,本文应运而生。一方面,它能助力刚接触 JavaScript 的初学者理解数组方法的用法与实际应用;另一方面,也能帮助已有一定基础的开发者巩固相关知识,从原理层面更深入地洞悉 JavaScript 数组方法的本质。
通过这篇文章你能学到什么?
- 概念理解 ------js中常见的数组方法梳理
- 原理探究 ------手写其中的几种数组方法
- 综合应用 ------结合题目和案例实际使用
一、概念理解
作为 JavaScript 的内置方法,本质上它们都是函数,而函数的核心无外乎两点:一是参数(即输入),二是返回值(即输出)。透过这两者,我们不仅能明晰方法的功能,更能掌握其正确用法。接下来,将以表格和代码结合的形式,从参数 、返回值 与功能三个维度,为大家系统梳理这些数组方法。
方法名称 | 参数含义 | 返回值 | 功能 |
---|---|---|---|
splice() | 接收三个参数:起始索引(必填)、要删除的元素个数(必填)、要插入的元素(可选) | 返回由被删除的元素组成的数组。如果没有删除任何元素,则返回空数组 | 在任意位置添加 / 删除元素 |
slice() | 接收两个参数:起始索引(可选,默认为 0)、结束索引(可选,默认为数组长度) | 返回一个新数组,包含从起始索引到结束索引(不包括结束索引)的元素 | 提取数组的一部分 |
concat() | 接收任意数量的数组或值作为参数,将它们连接到原数组的末尾 | 返回一个新数组,包含原数组和所有参数的值 | 合并多个数组 / 值 |
map() | 接收一个回调函数作为参数,回调函数接收三个参数:当前元素、索引、原数组(后两个可选) | 返回一个新数组,包含回调函数处理后的结果 | 对每个元素执行回调函数 |
filter() | 接收一个回调函数作为参数,该函数返回布尔值,用于筛选元素。回调函数接收三个参数:当前元素、索引、原数组(后两个可选) | 返回一个新数组,包含所有使回调函数返回 true 的元素 |
根据条件筛选元素 |
reduce() | 接收两个参数:回调函数(必填)和初始值(可选)。回调函数接收四个参数:累加器、当前元素、索引、原数组(后两个可选) | 返回最终的累加结果 | 累积计算数组元素为单个值 |
代码示例:
js
//splice
const arr = [1, 2, 3, 4, 5];
const removed = arr.splice(2, 2, 6, 7);
console.log(arr); // 输出: [1, 2, 6, 7, 5]
console.log(removed); // 输出: [3, 4]
//slice
const original = [10, 20, 30, 40, 50];
const sliced = original.slice(1, 3);
console.log(sliced); // 输出: [20, 30]
//concat
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = arr1.concat(arr2, 5, [6, 7]);
console.log(merged); // 输出: [1, 2, 3, 4, 5, 6, 7]
//map
const numbers = [1, 2, 3];
const squared = numbers.map(num => num * num);
console.log(squared); // 输出: [1, 4, 9]
// filter()
const ages = [25, 18, 30, 16];
const adults = ages.filter(age => age >= 18);
console.log(adults); // 输出: [25, 18, 30]
// reduce()
const sum = [1, 2, 3, 4].reduce((acc, curr) => acc + curr, 0);
console.log(sum); // 输出: 10
前面我们了解了数组方法的用法,但不少同学仍然可能好奇:filter ()
如何筛选元素?reduce ()
的累加逻辑是怎样的?map ()
为何不改变原数组?
接下来,我们就一起来看看它们内部是如何操作数组的,吃透原理、灵活运用。
二、原理探究
这里选择了filter
、map
、reduce
三种方法举例,因为他们分别代表了最常见的三种使用场景和功能,即筛选、映射 和聚合。同时这三个方法的手写难度也逐个递增,循序渐进。
首先明确,数组方法是定义在数组类的原型上的,也就是Array.prototype
,那么方法中的this就是指向调用方法的实例,也就是数组本身。这里不展开说明,不理解或者想深入的同学可以去了解一下js中的this指向。
2.1 filter方法手写
先梳理逻辑:
- 遍历原数组中的每一个索引:
this[i]
是元素,i
是索引,this
是数组本身 - 调用传入的回调函数:即调用
callback
- 根据回调函数的返回值决定是否加入新数组
- 返回新数组
那么可得代码如下
js
Array.prototype.myFliter=function(callback){
let newArr=[];
//遍历当前数组
for(let i=0;i<this.length;i++)
{
//将需要的参数传入回调函数并调用获取结果
if (callback(this[i],i,this))
{
newArr.push(this[i]);
}
}
return newArr;
}
拓展:这里的回调函数中的this默认指向全局对象(非严格模式)或者undefined(严格模式),如果使用者想改变回调函数中this的指向,还可以加一个可选参数来支持。代码如下:
js
Array.prototype.myFliter=function(callback,thisArg){
let newArr=[];
for(let i=0;i<this.length;i++)
{
//用call方法将回调函数绑定到用户传入的this中调用
if (callback.call(thisArg,this[i],i,this))
{
newArr.push(this[i]);
}
}
return newArr;
}
2.2 map方法手写
梳理逻辑:
- 遍历原有数组中的每一个索引:
this[i]
是元素,i
是索引,this
是数组本身 - 调用函数并把函数的结果添加进新数组
- 返回新数组
但是这里会有一个问题,那就是原数组并不是每一个索引都有对应的值的,即原数组可能是稀疏数组,所以在调用之前应该先判断此处是否有值。另外和filter方法一样,也支持传入回调函数的this值。代码如下:
js
Array.prototype.myMap=function(callback,thisArg){
let newArr=new Array(this.length);
for(let i=0;i<this.length;i++)
{
//判断是否为"空值"
if(i in this){
newArr.push(callback.call(thisArg,this[i],i,this));
}
}
return newArr;
}
2.2 reduce方法手写
梳理逻辑:
- 初始化累计结果:如果没传入则为第一个元素值
- 遍历原有数组中的每一个索引:
this[i]
是元素,i
是索引,this
是数组本身 - 调用函数并记录函数最终的累计结果
- 返回累计结果
可能大家很自然的也就想到从头到尾遍历数组,但是其实有一个小小的注意点,调用者是否传入初始值initialValue
不仅关系到累计结果的初始化,同样关系到遍历的起始点(传入了初始值是从0开始遍历,反之从1开始遍历),代码如下:
js
Array.prototype.myReduce=function(callback,initialValue){
//边界情况
if(this.length===0 && initialValue === undefined)
{
throw new TypeError("Reduce of empty array with no initial value");
}
//初始化累计结果和起始点
let result=initialValue ? initialValue : this[0];
let start=initialValue ? 0 : 1;
for(let i=start;i<this.length;i++)
{
callback(result,this[i],i,this);
}
return result;
}
亲手实现了这三个常用数组方法后,相信大家对数组方法的理解已不再停留在表面。有了这份实践基础,再去手写其他方法也能触类旁通。
三、综合应用
接下来,我们就进入实战应用环节。我会通过「注意要点 + 典型例题」的形式,帮大家梳理那些需要重点关注的核心问题,让知识真正落地。
3.1 注意方法的"返回值"------辨析其"功能"的联系和区别
实际使用和解题时,我们常不自觉地将两者混为一谈,觉得它们表达的是同一回事。当然,不乏返回值与功能统一的例子 ------ 比如用变量接收slice
的返回值,直接就能得到截取的片段,这种直观性让理解和使用都更顺畅。
但也有不少反直觉的情况:像与slice
名字相似的splice
,其核心功能是修改原数组(增删元素),但返回值却是被删除的元素组成的数组,和 "修改后数组" 这一功能结果完全分离。
因此使用时要注意两点:一是避免思维定式,不默认返回值就是功能的直接结果;二是学会利用这种 "差异",比如通过splice
的返回值快速获取被移除的元素,让方法为我们的需求服务。
例题:
-
数组元素的删除与获取 :已知数组
arr
,删除从目标值target
开始的length
个元素,并获取被删除的元素的数组。通过刚刚的讲解相信大家很快就能知道使用
filter
方法,比我们第一时间想到的遍历+维护新数组的方法会简单很多。 -
判断回文字符串
有的同学可能第一时间会想到双指针方法从头部和尾部遍历,但是从数组方法的角度思考,有没有什么方法可以从头部和尾部取得元素呢?答案是
pop
和shift
,需要注意的是,这二者不仅仅会弹出,还会返回弹出的元素,利用这一点就可以很简便的写出代码。
js
const _isPalindrome = string => {
let str = string.split('');
while (str.length > 1) {
//直接通过pop和shift方法获取而不需要额外变量接收
if (str.pop()!== str.shift()) {
return false;
}
}
return true;
};
3.2 注意方法的"破坏性"------是否改变原数组
这里的 "破坏性",指方法执行后是否直接修改原数组本身。这一特性直接影响代码逻辑:
-
破坏性方法 (如
splice
、sort
、reverse
)会就地修改原数组,执行后可直接使用修改后的数组,但原数组的初始状态会丢失; -
非破坏性方法 (如
slice
、map
、filter
)则不改变原数组,需通过返回值获取处理结果,原数组可保留用于后续操作。
实际场景中需特别留意:
-
从生产角度来看,若需要保留原始数据(如日志记录),用
slice
截取片段比splice
更安全,避免原数组被意外修改; -
从算法角度来看,有的题目会要求用
O(1)
的时间复杂度原地解决问题,这时候我们就应该选取一些"破坏性"的方法来完成题目的要求;
例题:
-
力扣hot100当中的题目,给定一个整数数组
nums
,将数组中的元素向右轮转k
个位置,要求原地解决,可以使用splice方法一行就巧妙的解决本题,给出代码:
js
var rotate = function(nums, k) {
//找到起始点再拼接
k=k%nums.length;
let start=nums.length-k===nums.length ? 0 : nums.length-k;
//注意执行顺序,先内层再外层,外层执行的时候数组已经切掉了k个元素
nums.splice(0, 0, ...nums.splice(start, k))
};
-
同样是力扣经典题目,需要原地移除所有数组中数值等于
val
的元素,精通数组方法的你或许一下就可以想到js内置的filter数组方法,但是不要忘了它是非破坏性的,不能满足题目要求。只能老老实实用双指针覆盖来完成这道题,代码如下:
js
var removeElement = function(nums, val) {
let left=0,right=0;
while(right<nums.length)
{
//不相等才更新,相等则继续
if(nums[right]!==val)
{
nums[left++]=nums[right];
}
right++;
}
return left;
};
四、总结
JavaScript 的数组方法繁多,很难用一篇文章穷尽所有细节。所以本文更想提供一套学习思路,帮你快速掌握并灵活运用:先从输入输出入手,弄清基础用法;再深入原理实现,理解底层逻辑;最终在实践中总结沉淀,形成自己的知识体系 ------ 这样就能逐步驾驭这些方法了。
另外,数组方法的学习并非孤立的。比如文中提到的 this
指向问题,其实是 JavaScript 整个语言体系的重要部分。学习时要注意知识的连通性,既要学得深,也要看得广,才能触类旁通。
总之,数组方法既带来了便捷,也暗藏着需要留意的细节。我们既要享受它简化代码的红利,也要在使用时保持细致,从细节处确保精准不出错 ------ 最后希望这篇文章能为正在看文章的你带来些许思考和帮助~