作为前端开发者,数组是我们日常开发和面试中最常接触的数据结构。但你真的懂JS数组吗?比如:为什么new Array(7).fill([])会踩坑?push和slice的本质区别是什么?forEach为啥不能中途break?
这篇文章把JS数组的核心知识点拆成7个实战模块,从基础创建到遍历、从纯函数到二维数组,全是干货,看完就能用,帮你彻底搞懂JS数组的底层逻辑和实战技巧。
一、先搞懂:JS数组和其他语言的不一样
其他语言的数组:要求元素类型一致、长度固定,内存布局严格连续。
JS数组:天生灵活,不用限制元素类型,也不用固定长度,开箱即用,但底层依然是「地址+偏移量」的内存逻辑。
这既是JS的优势,也是新手容易踩坑的点——灵活的背后,藏着不少容易忽略的细节。
二、数组创建:3种方式+避坑点
1. 最常用:字面量创建
constarr=[1,'a',true,{}];// 元素类型随意,无需指定长度console.log(arr);// [1, 'a', true, {}]✅ 优点:直观、灵活,日常开发首选。
2. 指定长度:new Array()
constarr=newArray(7);console.log(arr);// [empty × 7]console.log(arr[0]);// undefined⚠️ 踩坑提醒:
empty表示该位置未赋值,不属于任何数据类型;- 直接访问
arr[0]返回undefined,但和主动赋值undefined不是一回事。
3. 固定长度+固定值:fill()
// 正确用法:创建长度为7、每个元素都是1的数组constarr=newArray(7).fill(1);console.log(arr);// [1, 1, 1, 1, 1, 1, 1]// 踩坑用法:填充引用类型constbadArr=newArray(3).fill([]);badArr[0].push(1);console.log(badArr);// [[1], [1], [1]] (所有子数组都变了)⚠️ 核心坑点:
fill传入引用类型(如数组、对象)时,填充的是引用地址,所有位置共享同一个引用,改一个全变!
✅ 正确创建二维数组的方式:
constarr=newArray(3);constlen=arr.length;// 循环为每个位置创建新数组for(leti=0;i<len;i++){arr[i]=[];}arr[0].push(1);console.log(arr);// [[1], [], []] (仅第一个子数组变化)三、数组操作:纯函数vs非纯函数
JS数组的方法分两类:修改原数组(非纯函数)、返回新数组(纯函数),面试高频考点!
1. 非纯函数:修改原数组
| 方法 | 作用 | 返回值 |
|---|---|---|
| push | 尾部添加元素 | 新数组长度 |
| pop | 尾部删除元素 | 被删除的元素 |
| shift | 头部删除元素 | 被删除的元素 |
| unshift | 头部添加元素 | 新数组长度 |
constarr=[1,2,3];console.log(arr.push(4));// 4(返回长度)console.log(arr);// [1, 2, 3, 4](原数组被改)console.log(arr.pop());// 4(返回被删元素)console.log(arr);// [1, 2, 3](原数组被改)2. 纯函数:不修改原数组
| 方法 | 作用 | 返回值 |
|---|---|---|
| slice | 截取数组片段 | 新数组 |
| concat | 拼接数组 | 新数组 |
| map | 遍历映射新值 | 新数组 |
| filter | 筛选符合条件元素 | 新数组 |
| every | 检查所有元素是否符合条件 | 布尔值 |
| some | 检查是否有元素符合条件 | 布尔值 |
| reduce | 累计计算 | 最终累计值 |
constarr=[1,2,3,4,5,6,7];// map:映射新数组constdoubleArr=arr.map(item=>item*2);console.log(doubleArr);// [2, 4, 6, 8, 10, 12, 14]console.log(arr);// 原数组不变// filter:筛选元素constfilterArr=arr.filter(item=>item>3);console.log(filterArr);// [4, 5, 6, 7]// every:所有元素是否符合条件console.log(arr.every(item=>item%2===0));// false(不是所有数都是偶数)// some:至少一个元素符合条件console.log(arr.some(item=>item%2===0));// true(有偶数)// reduce:求和constsum=arr.reduce((pre,cur)=>pre+cur);console.log(sum);// 28四、数组遍历:5种方式+性能对比
遍历是数组最常用操作,不同场景选不同方式,性能和可读性天差地别!
1. for计数循环
constarr=[1,2,3,4,5];constlen=arr.length;// 优化:提前存长度,避免每次访问arr.lengthfor(leti=0;i<len;i++){console.log(arr[i]);}✅ 优点:性能最好(底层最接近机器指令);
❌ 缺点:可读性差,命令式写法。
2. for of循环
constarr=[1,2,3];for(constitemofarr){console.log(item);}✅ 优点:语义化好,简洁易读;
✅ 支持break/continue中途终止。
3. forEach方法
constarr=[1,2,3];arr.forEach((item,index,self)=>{console.log(item,index,self);// 元素、索引、原数组});✅ 优点:功能强,自带索引和原数组;
❌ 缺点:不能用break/continue终止循环,入调用栈,性能略差。
4. map方法
constarr=[1,2,3];constnewArr=arr.map(item=>item*2);✅ 场景:需要遍历并生成新数组;
❌ 注意:不要用map做纯遍历(浪费性能,forEach更合适)。
5. 二维数组遍历
constarr=newArray(3);constlen=arr.length;// 初始化二维数组for(leti=0;i<len;i++){arr[i]=[];}// 双层遍历for(leti=0;i<len;i++){for(letj=0;j<len;j++){console.log(arr[i][j],i,j);}}⚠️ 优化点:双层循环都提前存长度,避免多次访问arr.length,提升性能。
五、实战场景:这些坑90%的人都踩过
坑1:fill填充引用类型
// 错误constarr=newArray(3).fill([]);arr[0].push(1);// 所有子数组都变了// 正确constarr=newArray(3);for(leti=0;i<arr.length;i++){arr[i]=[];}坑2:混淆push的返回值
constarr=[1,2];constresult=arr.push(3);console.log(result);// 3(是长度,不是新数组!)console.log(arr);// [1, 2, 3]坑3:forEach用break终止循环
constarr=[1,2,3];// 报错:Uncaught SyntaxError: Illegal break statementarr.forEach(item=>{if(item===2)break;console.log(item);});✅ 替代方案:用for of循环,或提前return(仅终止当前迭代)。
六、总结
JS数组看似简单,实则藏着不少底层逻辑和实战坑:
- 创建数组时,引用类型填充用循环,别用fill;
- 操作数组时,分清纯函数和非纯函数,避免无意修改原数组;
- 遍历数组时,性能优先选for计数,可读性优先选for of,生成新数组选map;
- 二维数组遍历要提前存长度,优化性能。
掌握这些知识点,不管是日常开发避坑,还是面试应对hot100题,都能游刃有余。