从手写到应用——JavaScript数组方法总结

前言: 众所周知,JavaScript 中丰富的内置数组方法为数组操作赋予了极高的灵活性,让开发者能够更高效地处理数据。然而,这些五花八门的方法也时常让人在使用时感到眼花缭乱 ------ 比如,我们常会困惑:这个方法究竟该传入函数还是其他类型的参数?它的返回值是什么样的?调用后原数组会被修改,还是会保持原样?

为化解上述困惑,本文应运而生。一方面,它能助力刚接触 JavaScript 的初学者理解数组方法的用法与实际应用;另一方面,也能帮助已有一定基础的开发者巩固相关知识,从原理层面更深入地洞悉 JavaScript 数组方法的本质。

通过这篇文章你能学到什么?

  1. 概念理解 ------js中常见的数组方法梳理
  2. 原理探究 ------手写其中的几种数组方法
  3. 综合应用 ------结合题目和案例实际使用

一、概念理解

作为 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 () 为何不改变原数组?

接下来,我们就一起来看看它们内部是如何操作数组的,吃透原理、灵活运用。

二、原理探究

这里选择了filtermapreduce 三种方法举例,因为他们分别代表了最常见的三种使用场景和功能,即筛选、映射聚合。同时这三个方法的手写难度也逐个递增,循序渐进。

首先明确,数组方法是定义在数组类的原型上的,也就是Array.prototype,那么方法中的this就是指向调用方法的实例,也就是数组本身。这里不展开说明,不理解或者想深入的同学可以去了解一下js中的this指向

2.1 filter方法手写

先梳理逻辑:

  1. 遍历原数组中的每一个索引:this[i]是元素,i是索引,this是数组本身
  2. 调用传入的回调函数:即调用callback
  3. 根据回调函数的返回值决定是否加入新数组
  4. 返回新数组

那么可得代码如下

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方法手写

梳理逻辑:

  1. 遍历原有数组中的每一个索引:this[i]是元素,i是索引,this是数组本身
  2. 调用函数并把函数的结果添加进新数组
  3. 返回新数组

但是这里会有一个问题,那就是原数组并不是每一个索引都有对应的值的,即原数组可能是稀疏数组,所以在调用之前应该先判断此处是否有值。另外和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方法手写

梳理逻辑:

  1. 初始化累计结果:如果没传入则为第一个元素值
  2. 遍历原有数组中的每一个索引:this[i]是元素,i是索引,this是数组本身
  3. 调用函数并记录函数最终的累计结果
  4. 返回累计结果

可能大家很自然的也就想到从头到尾遍历数组,但是其实有一个小小的注意点,调用者是否传入初始值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的返回值快速获取被移除的元素,让方法为我们的需求服务。

例题:

  1. 数组元素的删除与获取 :已知数组 arr ,删除从目标值target开始的length个元素,并获取被删除的元素的数组。

    通过刚刚的讲解相信大家很快就能知道使用filter方法,比我们第一时间想到的遍历+维护新数组的方法会简单很多。

  2. 判断回文字符串

    有的同学可能第一时间会想到双指针方法从头部和尾部遍历,但是从数组方法的角度思考,有没有什么方法可以从头部和尾部取得元素呢?答案是popshift,需要注意的是,这二者不仅仅会弹出,还会返回弹出的元素,利用这一点就可以很简便的写出代码。

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 注意方法的"破坏性"------是否改变原数组

这里的 "破坏性",指方法执行后是否直接修改原数组本身。这一特性直接影响代码逻辑:

  • 破坏性方法 (如 splicesortreverse)会就地修改原数组,执行后可直接使用修改后的数组,但原数组的初始状态会丢失;

  • 非破坏性方法 (如 slicemapfilter)则不改变原数组,需通过返回值获取处理结果,原数组可保留用于后续操作。

实际场景中需特别留意:

  • 从生产角度来看,若需要保留原始数据(如日志记录),用 slice 截取片段比 splice 更安全,避免原数组被意外修改;

  • 从算法角度来看,有的题目会要求用O(1)的时间复杂度原地解决问题,这时候我们就应该选取一些"破坏性"的方法来完成题目的要求;

例题:

  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))
};
  1. 移除元素

    同样是力扣经典题目,需要原地移除所有数组中数值等于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 整个语言体系的重要部分。学习时要注意知识的连通性,既要学得深,也要看得广,才能触类旁通。

总之,数组方法既带来了便捷,也暗藏着需要留意的细节。我们既要享受它简化代码的红利,也要在使用时保持细致,从细节处确保精准不出错 ------ 最后希望这篇文章能为正在看文章的你带来些许思考和帮助~

相关推荐
工业甲酰苯胺9 分钟前
TypeScript枚举类型应用:前后端状态码映射的最简方案
javascript·typescript·状态模式
brzhang10 分钟前
我操,终于有人把 AI 大佬们 PUA 程序员的套路给讲明白了!
前端·后端·架构
止观止1 小时前
React虚拟DOM的进化之路
前端·react.js·前端框架·reactjs·react
goms1 小时前
前端项目集成lint-staged
前端·vue·lint-staged
谢尔登1 小时前
【React Natve】NetworkError 和 TouchableOpacity 组件
前端·react.js·前端框架
Lin Hsüeh-ch'in1 小时前
如何彻底禁用 Chrome 自动更新
前端·chrome
augenstern4163 小时前
HTML面试题
前端·html
张可3 小时前
一个KMP/CMP项目的组织结构和集成方式
android·前端·kotlin
G等你下课3 小时前
React 路由懒加载入门:提升首屏性能的第一步
前端·react.js·前端框架
谢尔登4 小时前
【React Native】ScrollView 和 FlatList 组件
javascript·react native·react.js