JavaScript 数组深度解析:从纯函数到二维数组陷阱,一文吃透前端数据结构核心

JavaScript 数组深度解析:从纯函数到二维数组陷阱,一文吃透前端数据结构核心

🚀 数组是前端开发中最常用的数据结构,但你真的了解它吗? 本文从内存模型、纯函数 vs 非纯函数、高阶方法到二维数组陷阱,结合 LeetCode Hot 100 面试视角,带你彻底搞懂 JS 数组的底层原理与实战技巧。

📖 前言

数组(Array)是几乎所有编程语言内置的数据结构,JavaScript 中的数组尤其灵活------不需要限定每项类型、不需要固定长度、API 丰富到让人眼花缭乱。

但灵活的背后也隐藏着陷阱:

  • ❓ push、pop 会修改原数组,怎么保证数据不可变?
  • ❓ forEach、map、filter 怎么选?
  • ❓ (new Array(7)).fill([]) 为什么所有行会变成同一个数组?
  • ❓ 纯函数和非纯函数有什么区别?

这篇文章将带你从内存模型 到面试实战,彻底掌握 JS 数组的核心知识。


🧠 知识图谱

scss 复制代码
JS 数组深度解析
├── 📦 一、数组的本质与内存模型
│   ├── 连续内存空间
│   ├── 起始地址 + 偏移量
│   └── ADT(抽象数据类型)
│
├── 🛠️ 二、数组的创建方式
│   ├── 字面量 []
│   ├── new Array()
│   ├── new Array(7).fill()
│   └── 二维数组初始化陷阱
│
├── 🔄 三、修改原数组的方法(非纯函数)
│   ├── push / pop
│   ├── unshift / shift
│   └── 纯函数 vs 非纯函数
│
├── ✨ 四、不修改原数组的方法(纯函数)
│   ├── map --- 映射
│   ├── filter --- 筛选
│   ├── every / some --- 判断
│   └── reduce --- 归约
│
├── 🏃 五、数组遍历方法对比
│   ├── for
│   ├── for...of
│   ├── forEach
│   └── 怎么选?
│
└── 🕳️ 六、二维数组与引用类型陷阱
    ├── 矩阵结构
    ├── fill([]) 的坑
    └── 正确初始化方式

📦 一、数组的本质与内存模型

1.1 什么是数组?

💡 数组是一段连续的内存空间 ,通过起始地址 + 偏移量来访问元素。

css 复制代码
内存中的数组:

起始地址:0x1000
┌─────────┬─────────┬─────────┬─────────┐
│  arr[0] │  arr[1] │  arr[2] │  arr[3] │
│   'a'   │   'b'   │   'c'   │    1    │
├─────────┼─────────┼─────────┼─────────┤
│ 0x1000  │ 0x1008  │ 0x1010  │ 0x1018  │  ←── 起始地址 + 索引 × 元素大小
└─────────┴─────────┴─────────┴─────────┘

访问 arr[2]:
  地址 = 起始地址 + 2 × 元素大小
  地址 = 0x1000 + 2 × 8 = 0x1010
  → 直接定位,O(1) 时间复杂度!

1.2 ADT:抽象数据类型

数组作为一种 ADT(Abstract Data Type,抽象数据类型),具有两个核心特征:

特征 说明
连续的存储空间 元素在内存中连续排列
特定的操作 push、pop、map、filter 等

💡 JS 数组的特殊性:虽然底层实现可能不是严格的连续内存(V8 引擎有优化),但从使用角度看,它表现出数组的所有特征。


🛠️ 二、数组的创建方式

2.1 字面量创建(最常用)

javascript 复制代码
// ✅ 最常用:方括号 + 元素
const arr = [1, 2, 3, 4];
const strArr = ['a', 'b', 'c'];

// JS 数组更灵活:不需要每项类型一样
const mixed = [1, 'hello', true, { a: 1 }];

2.2 new Array() 创建

javascript 复制代码
// 空数组
const arr1 = new Array();        // → []

// 指定长度(元素为空)
const arr2 = new Array(7);       // → [empty × 7]

// 指定元素
const arr3 = new Array(1, 2, 3); // → [1, 2, 3]

⚠️ 注意 :new Array(7) 创建的是长度为 7 的空数组 ,不是 [7]!

2.3 fill() 填充数组

javascript 复制代码
// 创建并填充
const arr = (new Array(7)).fill(1);
// → [1, 1, 1, 1, 1, 1, 1]

// 理解 empty
const emptyArr = new Array(7);
console.log(emptyArr);        // → [empty × 7]
console.log(emptyArr[0]);     // → undefined
console.log(0 in emptyArr);   // → false(这个位置还没有被占据)
状态 说明
empty 数组位置未被占据,不属于任何类型
undefined 位置被占据,值为 undefined

🔄 三、修改原数组的方法(非纯函数)

3.1 四个核心方法

javascript 复制代码
const arr = ['a', 'b', 'c'];

// push:尾部添加,返回新长度
arr.push(1);       // → 4(返回长度)
arr.push(2);       // arr = ['a', 'b', 'c', 1, 2]

// unshift:头部添加,返回新长度
arr.unshift(3);    // → 6(返回长度)
                   // arr = [3, 'a', 'b', 'c', 1, 2]

// pop:尾部删除,返回被删除的元素
arr.pop();         // → 2(返回被删除的值)
                   // arr = [3, 'a', 'b', 'c', 1]

// shift:头部删除,返回被删除的元素
arr.shift();       // → 3(返回被删除的值)
                   // arr = ['a', 'b', 'c', 1]
方法 操作位置 功能 返回值
push(x) 尾部 添加元素 新数组长度
pop() 尾部 删除元素 被删除的元素
unshift(x) 头部 添加元素 新数组长度
shift() 头部 删除元素 被删除的元素

3.2 纯函数 vs 非纯函数

这是函数式编程中的核心概念:

javascript 复制代码
// ❌ 非纯函数:依赖外部变量,修改外部状态
let num = 0;
function add(b) {
    num += b;      // 修改外部变量
    return num;    // 结果不可控
}
add(1);  // → 1
add(1);  // → 2(同样的输入,不同的输出!)

// ✅ 纯函数:不依赖外部状态,相同输入永远相同输出
function pureAdd(a, b) {
    return a + b;  // 只依赖参数,不修改外部
}
pureAdd(1, 2);  // → 3
pureAdd(1, 2);  // → 3(永远相同)
特性 纯函数 非纯函数
副作用 ❌ 无 ✅ 有(修改外部状态)
可预测性 ✅ 相同输入相同输出 ❌ 结果不可控
可测试性 ✅ 容易测试 ❌ 难以测试
并发安全 ✅ 线程安全 ❌ 可能冲突

💡 数组方法的分类:

  • 非纯函数 :push、pop、shift、unshift、splice、sort、reverse(修改原数组)
  • 纯函数 :map、filter、slice、concat、reduce(返回新数组)

✨ 四、不修改原数组的方法(纯函数)

4.1 map ------ 映射

javascript 复制代码
const arr = [1, 2, 3, 4, 5];

// 每一项乘以 2
const newArr = arr.map((item, index, self) => {
    return item * 2;
});

console.log(arr);      // → [1, 2, 3, 4, 5](原数组不变!)
console.log(newArr);   // → [2, 4, 6, 8, 10]
参数 说明
item 当前元素
index 当前索引
self 原数组本身

4.2 filter ------ 筛选

javascript 复制代码
const arr = [1, 2, 3, 4, 5];

// 筛选大于 2 的元素
const filtered = arr.filter((item) => {
    return item > 2;
});

console.log(filtered);  // → [3, 4, 5]

💡 filter 规则 :函数返回 true 的元素留下,返回 false 的过滤掉。

4.3 every / some ------ 判断

javascript 复制代码
const arr = [1, 2, 3, 4, 5];

// every:每一项都满足条件才返回 true
console.log(arr.every((item) => item > 0));   // → true(都大于 0)
console.log(arr.every((item) => item > 3));   // → false(1,2,3 不满足)

// some:有一项满足条件就返回 true
console.log(arr.some((item) => item > 2));    // → true(3,4,5 满足)
console.log(arr.some((item) => item > 10));   // → false(都不满足)
方法 条件 返回 true 的情况
every 所有项 每一项都满足
some 任意一项 至少一项满足

4.4 reduce ------ 归约

javascript 复制代码
const arr = [1, 2, 3, 4, 5];

// 求和
const sum = arr.reduce((pre, cur, index) => {
    console.log(pre, cur, index);
    return pre + cur;
}, 0);  // 0 是初始值

// 执行过程:
// 0  1  0   ← pre=0(初始值), cur=1, index=0
// 1  2  1   ← pre=1, cur=2, index=1
// 3  3  2   ← pre=3, cur=3, index=2
// 6  4  3   ← pre=6, cur=4, index=3
// 10 5  4   ← pre=10, cur=5, index=4
// 最终结果:15
参数 说明
pre 累加器(上一次回调的返回值)
cur 当前元素
index 当前索引
初始值 reduce 的第二个参数(可选但推荐)

⚠️ 初始值的重要性 :如果不传初始值,pre 第一次是 arr[0],cur 是 arr[1],空数组会报错!

4.5 高阶方法对比总结

方法 功能 返回值 是否纯函数
map 映射转换 新数组 ✅
filter 筛选过滤 新数组 ✅
every 全部满足? boolean ✅
some 部分满足? boolean ✅
reduce 归约汇总 任意值 ✅
forEach 遍历执行 undefined ✅(不返回值)

🏃 五、数组遍历方法对比

5.1 四种遍历方式

javascript 复制代码
const arr = ['a', 'b', 'c'];

// ① for 计数循环
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

// ② for...of(ES6)
for (const item of arr) {
    console.log(item);
}

// ③ forEach
arr.forEach((item, index, self) => {
    console.log(item, index, self);
});

// ④ map(如果需要用返回值)
arr.map((item) => {
    console.log(item);
    return item;
});

5.2 怎么选?

方法 优点 缺点 适用场景
for 性能最好,可 break/continue 可读性一般 性能敏感、需要中断
for...of 语义清晰,可 break/continue 无索引 只关心元素值
forEach 功能强大,代码简洁 不能中断(break) 简单遍历操作
map 返回新数组,链式调用 必须返回 需要转换数据

💡 选择建议:

  • 需要 break/continue → 用 for 或 for...of
  • 只需要遍历 → 用 forEach
  • 需要转换数据 → 用 map
  • 需要筛选 → 用 filter

5.3 forEach 不能中断

javascript 复制代码
const arr = [1, 2, 3, 4, 5];

// ❌ forEach 中不能用 break
arr.forEach((item) => {
    if (item === 3) {
        break;  // SyntaxError: Illegal break statement
    }
});

// ✅ 用 for...of 可以 break
for (const item of arr) {
    if (item === 3) {
        break;  // 正常中断
    }
}

🕳️ 六、二维数组与引用类型陷阱

6.1 什么是二维数组?

css 复制代码
二维数组(矩阵):

    0   1   2
  ┌───┬───┬───┐
0 │ 1 │ 2 │ 3 │
  ├───┼───┼───┤
1 │ 4 │ 5 │ 6 │
  ├───┼───┼───┤
2 │ 7 │ 8 │ 9 │
  └───┴───┴───┘

arr[0][0] = 1   arr[0][1] = 2   arr[0][2] = 3
arr[1][0] = 4   arr[1][1] = 5   arr[1][2] = 6
arr[2][0] = 7   arr[2][1] = 8   arr[2][2] = 9

6.2 二维数组的创建陷阱

javascript 复制代码
// ❌ 错误方式:fill([]) 的坑
const arr = (new Array(7)).fill([]);
// 创建了一个包含 7 个引用的数组
// 这 7 个引用都指向**同一个**空数组!

arr[0][0] = 1;
console.log(arr);
// → [[1], [1], [1], [1], [1], [1], [1]]
// 修改一个,全部变了!

为什么会这样?

scss 复制代码
错误方式:(new Array(7)).fill([])

内存模型:
┌─────────┬─────────┬─────────┬─────────┐
│ arr[0]  │ arr[1]  │ arr[2]  │  ...    │
│   │     │   │     │   │     │         │
│   │     │   │     │   │     │         │
│   └─────┴───┘     └───┘     │         │
│         │                   │         │
│         ▼                   │         │
│      ┌─────┐                │         │
│      │ []  │ ←──────────────┘         │
│      └─────┘                          │
│      (同一个数组对象!)               │
└───────────────────────────────────────┘

所有 7 个位置都指向同一个 [] 对象
修改 arr[0][0] 等于修改所有行的第一个元素

6.3 正确初始化二维数组

javascript 复制代码
// ✅ 正确方式:循环创建独立的子数组
const arr = new Array(7);
const len = arr.length;

for (let i = 0; i < len; i++) {
    arr[i] = [];  // 每次创建新的空数组
}

arr[0][0] = 1;
console.log(arr);
// → [[1], [], [], [], [], [], []]
// 只修改了第一行!

正确方式的内存模型:

css 复制代码
正确方式:循环创建

内存模型:
┌─────────┬─────────┬─────────┬─────────┐
│ arr[0]  │ arr[1]  │ arr[2]  │  ...    │
│   │     │   │     │   │     │         │
│   ▼     │   ▼     │   ▼     │         │
│ ┌───┐   │ ┌───┐   │ ┌───┐   │         │
│ │[1]│   │ │[] │   │ │[] │   │         │
│ └───┘   │ └───┘   │ └───┘   │         │
│ 独立    │  独立   │  独立   │         │
└─────────┴─────────┴─────────┴─────────┘

每个位置指向不同的数组对象
修改 arr[0] 不会影响其他行

6.4 二维数组的遍历

javascript 复制代码
const arr = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

// 嵌套循环遍历
const outerLen = arr.length;
for (let i = 0; i < outerLen; i++) {
    const innerLen = arr[i].length;
    for (let j = 0; j < innerLen; j++) {
        console.log(arr[i][j]);
    }
}

// 优化:缓存 length,避免重复访问属性
// arr.length 和 arr[i].length 是属性访问,有性能开销

💡 性能优化 :在循环中缓存 arr.length 和 arr[i].length,避免每次迭代都进行属性查找。


📊 核心知识速查表

数组方法分类

类别 方法 修改原数组? 返回值
添加 push ✅ 新长度
添加 unshift ✅ 新长度
删除 pop ✅ 被删元素
删除 shift ✅ 被删元素
映射 map ❌ 新数组
筛选 filter ❌ 新数组
判断 every ❌ boolean
判断 some ❌ boolean
归约 reduce ❌ 任意值
遍历 forEach ❌ undefined

纯函数 vs 非纯函数

纯函数 非纯函数
map push
filter pop
slice shift
concat unshift
reduce splice
sort
reverse

💡 学习建议

  1. 优先使用纯函数 :map、filter、reduce 不修改原数组,代码更安全、更可预测
  2. 注意 fill 的引用陷阱 :fill([])、fill({}) 都会让所有位置指向同一个对象
  3. 缓存 length 优化性能 :在循环中先 const len = arr.length
  4. 选择正确的遍历方式 :需要中断用 for/for...of,简单遍历用 forEach
  5. 面试准备:LeetCode Hot 100 中大量题目涉及数组操作,熟练掌握这些方法

📚 推荐阅读

  • MDN - Array
  • MDN - 纯函数
  • LeetCode Hot 100 --- 数组相关题目
  • 《JavaScript 高级程序设计》第 4 版 --- 数组章节
  • 《数据结构与算法 JavaScript 描述》--- 数组与列表

🏷️ 标签 :JavaScript 数组 数据结构 纯函数 map filter reduce 二维数组 前端 面试


如果这篇文章帮你理清了 JS 数组的核心知识,欢迎点赞 + 收藏 + 关注!有任何疑问欢迎在评论区交流~ 🎉

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
m0_547486663 天前
《数据结构教程》全套 PPT课件2026
数据结构
李少兄3 天前
JavaScript 隐式全局变量解析
javascript
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端