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

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

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

📖 前言

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

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

  • pushpop 会修改原数组,怎么保证数据不可变?
  • forEachmapfilter 怎么选?
  • (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(永远相同)
特性 纯函数 非纯函数
副作用 ❌ 无 ✅ 有(修改外部状态)
可预测性 ✅ 相同输入相同输出 ❌ 结果不可控
可测试性 ✅ 容易测试 ❌ 难以测试
并发安全 ✅ 线程安全 ❌ 可能冲突

💡 数组方法的分类

  • 非纯函数pushpopshiftunshiftsplicesortreverse(修改原数组)
  • 纯函数mapfiltersliceconcatreduce(返回新数组)

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

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]curarr[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 → 用 forfor...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.lengtharr[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. 优先使用纯函数mapfilterreduce 不修改原数组,代码更安全、更可预测
  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 数组的核心知识,欢迎点赞 + 收藏 + 关注!有任何疑问欢迎在评论区交流~ 🎉

相关推荐
万少2 小时前
一封邮件,让我重新打开了搁置半年的鸿蒙应用
前端·javascript·后端
wjj不想说话2 小时前
你的小程序活动页,可能已经成了后台配置的杂物间
前端
梦想是准点下班2 小时前
androidStudio打包,我又又又忘了
前端
槑有老呆2 小时前
栈队列链表,三个故事就懂了
前端
ViavaCos2 小时前
pnpm v11 的安全策略,让我踩了个坑
前端
To_OC2 小时前
从一段定时器代码,重新捋清 JS 同步、异步与 Promise
前端·javascript·代码规范
持敬chijing2 小时前
Web渗透之前后端漏洞-XSS漏洞原理攻击防御全流程
前端·安全·web安全·网络安全·网络攻击模型·安全威胁分析·xss
程序员黑豆3 小时前
AI全栈开发 - Java:注释
前端·后端·ai编程
痕忆丶3 小时前
Typora 的替代marktext,marktext切换中文
前端