在前端开发中,数组是最基础也最核心的数据结构之一,几乎所有业务场景都离不开它的身影。但你真的懂数组吗?从创建时的内存考量,到遍历中的性能差异,再到容易踩坑的闭包问题,每一个细节都藏着提升代码效率的关键。本文结合实战代码,带你从底层逻辑到实战用法,彻底吃透 JS 数组。
一、先明确:数组属于什么数据结构?
要理解数组的特性,先从数据结构分类入手,这能帮我们从本质上判断数组的适用场景:
- 线性数据结构:元素按顺序排列,每个元素只有一个前驱和一个后继,包括数组、栈(先进后出)、队列(先进先出)、链表(离散存储)。
- 非线性数据结构:元素关系不唯一,如树(二叉树)、图等,适用于复杂关系存储。
而数组作为线性结构的代表,核心优势是 连续内存存储,这也是它访问速度快的根本原因;但短板也源于此 —— 动态扩容时的内存开销,这一点我们后面详细说。
二、数组创建:初始化的门道与取舍
数组的创建看似简单,但不同场景的选择,直接影响内存效率。
1. 已知元素:直接字面量创建
如果明确知道数组内的元素,直接用字面量写法最简洁,无需额外内存浪费:
javascript
运行
const arr = [1, 2, 3, 4, 5, 6];
这种方式创建的数组,内存直接分配对应长度的连续空间,访问和操作都高效。
2. 未知元素:初始化空数组
如果不确定元素内容,只知道大致长度,用 new Array(length).fill(value) 初始化:
javascript
运行
// 创建长度为 10、每个元素都是 0 的数组
const arr = new Array(10).fill(0);
这里要注意两个关键问题:
- 内存浪费与不足:固定长度初始化时,长度设多了会浪费内存,设少了需要二次扩容。
- JS 数组的动态扩容机制:JS 数组本质是 “动态数组”,当元素超出初始长度时,会自动申请更大的连续内存(通常是原长度的 1.5~2 倍),并将原数组元素 “搬家” 到新内存。这个过程会产生额外开销,这也是数组在动态添加大量元素时,性能不如链表的原因。
数组 vs 链表的核心取舍:
- 少量数据、频繁访问:数组更优(连续内存,访问时间复杂度 O (1))。
- 大量数据、频繁增删:链表更优(无需扩容,增删时间复杂度 O (1)),但链表每个节点需要额外存储指针,内存开销更高。
三、数组遍历:5 种方法对比,性能与场景全解析
遍历是数组最常用的操作,但不同遍历方法的性能、灵活性天差地别。下面结合代码,逐一拆解优缺点和适用场景。
1. 计数循环(for 循环):性能天花板
javascript
运行
const arr = new Array(6).fill(0);
const len = arr.length; // 提前缓存长度,减少对象属性访问开销
// 与 CPU 执行逻辑契合,无额外函数调用开销
for (let i = 0; i < len; i++) {
console.log(arr[i]);
}
核心优势:
- 性能最优:直接通过索引访问元素,无函数入栈出栈、作用域切换等额外开销,和 CPU 底层执行逻辑高度匹配。
- 灵活控制:支持
break中断循环、continue跳过当前迭代,自由度最高。
不足:
- 可读性稍差:需要手动管理索引(
i的初始化、判断条件、自增),代码略显繁琐。
适用场景:
- 对性能要求高的场景(如大数据量遍历)。
- 需要灵活控制循环流程(中断、跳过)的场景。
2. forEach:简洁但受限
javascript
运行
const arr = [1, 2, 3, 4, 5, 6];
// 无法用 break/continue 控制循环,return 仅跳过当前元素
arr.forEach((item, index) => {
if (item === 3) {
// break; // 报错:非法使用 break
return; // 仅跳过当前元素,继续遍历 4、5、6
}
console.log(item); // 输出 1、2、4、5、6
});
核心优势:
- 代码简洁:无需管理索引,直接获取元素和索引,语义清晰。
不足:
- 性能较差:回调函数每次执行都会产生函数入栈出栈开销,比计数循环慢。
- 无法中断:不能用
break终止循环,return仅相当于continue,灵活性不足。
适用场景:
- 简单遍历,无需控制循环流程,追求代码简洁性。
3. map:遍历 + 转换,生成新数组
javascript
运行
const arr = [1, 2, 3, 4, 5, 6];
// 遍历的同时加工元素,返回新数组(原数组不变)
const newArr = arr.map(item => item + 1);
console.log(newArr); // 输出 [2, 3, 4, 5, 6, 7]
console.log(arr); // 原数组不变:[1, 2, 3, 4, 5, 6]
核心优势:
- 兼顾遍历与转换:自动收集回调函数的返回值,生成新数组,适合数据格式化(如提取对象属性、数值转换)。
- 不修改原数组:符合 “纯函数” 思想,减少副作用。
注意点:
- 必须返回值:若回调函数无
return,新数组对应位置会是undefined。 - 不可中断:和
forEach一样,无法用break终止。
适用场景:
- 需要基于原数组生成新数组(如数据转换、属性提取)。
4. for...of:ES6 优雅遍历,兼顾可读性与灵活性
javascript
运行
const arr = [1, 2, 3, 4, 5, 6];
// 直接获取元素,无需索引,可读性强
for (let item of arr) {
if (item === 3) break; // 支持 break 中断
console.log(item); // 输出 1、2
}
核心优势:
- 可读性强:直接遍历元素,代码简洁直观,比计数循环易读。
- 灵活控制:支持
break、continue,兼顾优雅与灵活性。 - 适用范围广:可遍历数组、字符串、
Map、Set等所有可迭代对象。
不足:
- 性能略逊于计数循环:但优于
forEach,日常开发中性能足够。
适用场景:
- 追求代码优雅,需要控制循环流程,无需索引的场景。
5. for...in:遍历对象专用,数组慎用
javascript
运行
// 1. 遍历对象(设计初衷)
const obj = { name: 'zp', age: 18, hobbies: ['篮球', '足球'] };
for (let key in obj) {
console.log(key, obj[key]); // 输出属性名和属性值
}
// 2. 遍历数组(不推荐)
const arr = [1, 2, 3, 4, 5, 6];
for (let key in arr) {
console.log(key, arr[key]); // key 是字符串类型的索引(如 "0")
}
核心问题:
- 遍历数组时,
key是字符串类型,若用于数值计算需手动转换。 - 会遍历数组原型链上的自定义属性(如
Array.prototype.custom = 1,会被遍历到)。 - 遍历顺序不固定(可能不是按索引顺序)。
适用场景:
- 仅用于遍历对象的可枚举属性,数组遍历优先选择其他方法。
遍历方法总结表
| 方法 | 性能 | 能否中断 | 适用场景 | 核心特点 |
|---|---|---|---|---|
| 计数循环 | 最优 | 是 | 大数据量、需控制流程 | 手动管理索引,性能天花板 |
| forEach | 较差 | 否 | 简单遍历,无需控制流程 | 语义清晰,无索引管理 |
| map | 较差 | 否 | 遍历 + 转换,生成新数组 | 不修改原数组,返回新数组 |
| for...of | 中等 | 是 | 优雅遍历,需控制流程 | 直接取元素,支持多对象类型 |
| for...in | 较差 | 是 | 遍历对象属性 | 数组慎用,遍历原型链属性 |
四、经典坑:setTimeout 中的数组遍历闭包问题
这是前端面试高频题,也是理解作用域的关键,结合代码带你看透本质:
javascript
运行
// 代码示例
for (let i = 0; i < 10; i++) {
// 定时器延迟 1 秒执行回调
setTimeout(function () {
console.log(i); // 输出 0、1、2、...、9
}, 1000);
}
为什么不是输出 10 个 10?
- 关键在于
let的块级作用域:for循环中用let声明的i,每个循环迭代都会创建一个独立的 “词法环境”,i属于当前循环块,而非全局。 - 定时器的回调函数会捕获当前循环块的
i,因此每个回调函数访问的都是各自迭代的i,延迟 1 秒后依次输出 0 到 9。
若把 let 换成 var(面试常考对比):
javascript
运行
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i); // 输出 10 个 10
}, 1000);
}
var没有块级作用域,i是全局 / 函数级变量,所有回调函数共享同一个i。- 定时器回调会在循环结束后(1 秒后)执行,此时
i已经循环到 10,因此所有回调都输出 10。
核心结论:
- 循环中使用定时器、事件监听等异步操作时,用
let声明循环变量,可避免闭包导致的变量共享问题。 - 本质是块级作用域(
let)与函数作用域(var)的差异,以及闭包对外部变量的捕获机制。
五、数组使用终极建议
-
创建数组:已知元素用字面量,未知元素用
fill初始化,提前评估长度减少扩容开销。 -
遍历选择:
- 性能优先选计数循环;
- 简洁遍历选
forEach; - 数据转换选
map; - 优雅可控选
for...of; - 遍历对象选
for...in(数组慎用)。
-
避坑要点:循环异步操作用
let声明变量,避免for...in遍历数组。
数组作为 JS 中最基础的数据结构,吃透这些细节不仅能提升代码效率,更能加深对作用域、闭包等核心概念的理解。希望本文的干货能帮你在开发中少踩坑、写出更优雅的代码!