JS 数组指南:从内存原理到二维矩阵

前言:数组是 JS 里最常用的数据结构,但它的连续内存布局、增删方法的副作用、纯函数遍历族、以及二维数组的 fill 陷阱,你真的吃透了吗?本文从 ADT 概念出发,逐层拆解到二维矩阵。


目录

  1. [数组的底层:连续内存与 ADT](#数组的底层:连续内存与 ADT "#%E4%B8%80%E6%95%B0%E7%BB%84%E7%9A%84%E5%BA%95%E5%B1%82%E8%BF%9E%E7%BB%AD%E5%86%85%E5%AD%98%E4%B8%8E-adt")
  2. [增删四兄弟:push / pop / shift / unshift](#增删四兄弟:push / pop / shift / unshift "#%E4%BA%8C%E5%A2%9E%E5%88%A0%E5%9B%9B%E5%85%84%E5%BC%9Fpush--pop--shift--unshift")
  3. 纯函数与非纯函数
  4. [遍历六法:从 for 到 reduce](#遍历六法:从 for 到 reduce "#%E5%9B%9B%E9%81%8D%E5%8E%86%E5%85%AD%E6%B3%95%E4%BB%8E-for-%E5%88%B0-reduce")
  5. [二维数组与 fill 陷阱](#二维数组与 fill 陷阱 "#%E4%BA%94%E4%BA%8C%E7%BB%B4%E6%95%B0%E7%BB%84%E4%B8%8E-fill-%E9%99%B7%E9%98%B1")
  6. 总结

一、数组的底层:连续内存与 ADT

数组是几乎所有编程语言都内置的数据结构。在 JavaScript 里,数组尤其灵活:不要求每一项类型一致,也不需要预先声明长度。

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

但灵活不等于没有原理。数组在内存中的本质是一段连续的存储空间 ------每个元素紧挨着下一个,通过"起始地址 + 偏移量"一步算出任何元素的物理位置。这是它能够 O(1) 随机访问的根本原因。

ADT:数据结构 = 存储 + 操作

数据结构课里有一个关键概念叫 ADT(Abstract Data Type,抽象数据类型) ------任何数据结构都包含两部分:存储方式 + 特定操作 。数组的存储方式是连续内存,而它的特定操作就是那四个耳熟能详的方法:pushpopshiftunshift

Array 的原型链

在 JS 的"一切皆对象"体系下,Array 本身也是一个对象。创建一个数组时发生了什么?

javascript 复制代码
const arr = new Array(); // 等价于 []
console.log(typeof Array);                           // "function"
console.log(Array.prototype);                        // 数组的所有方法所在地
console.log(Array.prototype.__proto__.constructor);  // Object
console.log(Array.prototype.__proto__.__proto__);    // null

原型链路径:实例 arr → Array.prototype → Object.prototype → null。所有数组方法(pushmapforEach......)都挂在 Array.prototype 上,实例通过 __proto__ 找到它们。


二、增删四兄弟:push / pop / shift / unshift

数组的四个基础操作恰好对应队尾队头的增删:

perl 复制代码
         unshift ← 队头 → shift
        ┌────┬────┬────┬────┐
        │ 3  │ a  │ b  │ c  │
        └────┴────┴────┴────┘
         push  → 队尾 ← pop
javascript 复制代码
const arr = ['a', 'b', 'c'];

arr.push(1);        // 队尾入 → ['a','b','c',1],返回新长度 4
arr.push(2);        // 队尾入 → ['a','b','c',1,2],返回新长度 5
arr.unshift(3);     // 队头入 → [3,'a','b','c',1,2],返回新长度 6
arr.pop();          // 队尾出 → 返回 2,arr 变为 [3,'a','b','c',1]
arr.shift();        // 队头出 → 返回 3,arr 变为 ['a','b','c',1]
方法 位置 方向 返回值 副作用
push(x) 队尾 新长度 修改原数组
pop() 队尾 移除的值 修改原数组
unshift(x) 队头 新长度 修改原数组
shift() 队头 移除的值 修改原数组

注意 push / unshift 返回的是新 length ,而 pop / shift 返回的是被移除的元素本身。这是新手最容易搞混的细节。

这四个方法共同的缺点是:会直接修改原数组。很多场景下我们不想破坏原始数据,这就引出了纯函数的概念。


三、纯函数与非纯函数

看一段对比代码:

javascript 复制代码
let num = 0;
function add(b) {
    num += b;          // 修改了外部的 num
    return num;
}

add(3);  // num = 3
add(3);  // num = 6 ------ 同样的输入,不同的输出!

add(3) 调用两次,第一次返回 3,第二次返回 6------相同的输入产生了不同的输出。因为它依赖并修改了外部变量 num ,这就是非纯函数

纯函数的定义很简单:同样的输入永远产生同样的输出,且没有副作用(不修改外部状态)。

纯函数 非纯函数
同样输入 → 同样输出 ❌(受外部状态影响)
修改原数据 ❌ 不改 ✅ 会改
代表方法 map / filter / reduce push / pop / shift / unshift
适用场景 数据处理流水线 队列、栈等有状态场景

回到数组:pushpop 天然是非纯函数------它们修改了原数组。而在数据加工场景(如格式化 API 返回结果、筛选列表),我们应该倾向纯函数。


四、遍历六法:从 for 到 reduce

数组的遍历方法构成了一个从"机器化"到"语义化"的光谱:

创建一个初始数组

javascript 复制代码
const arr = (new Array(7)).fill(1);  // 长度为 7,每个元素都为 1
console.log(arr);  // [1, 1, 1, 1, 1, 1, 1]

new Array(7) 创建一个长度为 7 的空数组------7 个位置都处于 empty 状态,未被任何值占据。.fill(1) 把它们全部初始化为 1

遍历六法对比

以一个数值数组为例:

javascript 复制代码
const arr = [6, 8, 12, 15];

① for 计数循环

javascript 复制代码
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

命令式写法,可读性一般,但性能最好------没有函数调用开销。

② forEach

javascript 复制代码
arr.forEach((item, index, self) => {
    console.log(item, index, self);
});

语义清晰,功能强大(回调拿到 item / index / 原数组),但不能中途 break

③ map ------ 映射转换

javascript 复制代码
const doubled = arr.map(item => item * 2);
console.log(doubled);  // [12, 16, 24, 30]
console.log(arr);      // [6, 8, 12, 15] ← 原数组不变

纯函数,返回一个新数组,每个元素是回调的返回值。

④ filter ------ 条件筛选

javascript 复制代码
const evens = arr.filter(item => item % 2 === 0);
console.log(evens);  // [6, 8, 12]

纯函数 ,回调返回 true 的元素留下,组成新数组。

⑤ every / some ------ 布尔判断

javascript 复制代码
arr.every(item => item % 2 === 0);  // false(15 不是偶数)
arr.some(item => item % 2 === 0);   // true(至少有一个偶数)

every:全真则真。some:有一真即真。

⑥ reduce ------ 归并汇总

javascript 复制代码
const sum = arr.reduce((prev, item, index) => {
    // prev: 上次回调的返回值
    // item: 当前元素
    return prev + item;
}, 0);  // 0 是初始值
console.log(sum);  // 41

reduce 是最强大的遍历方法------求和、拼字符串、转对象、连 Promise 链,都能用它写。

怎么选?

场景 推荐方法
追求极限性能 for 计数循环
每个元素执行操作、不需 break forEach
把 A 数组转换为 B 数组 map
按条件筛选子集 filter
判断是否全满足/存在满足 every / some
汇总成一个值(求和、拼接等) reduce

五、二维数组与 fill 陷阱

二维数组常见于矩阵运算------而 LLM 底层的向量计算正是大规模的矩阵运算。

fill(\[\]) 的经典坑

直觉上,你可能会这样创建一个 7 行的二维数组:

javascript 复制代码
const arr = (new Array(7)).fill([]);
arr[0][0] = 1;
console.log(arr);
// 预期:[[1], [], [], [], [], [], []]
// 实际:[[1], [1], [1], [1], [1], [1], [1]]

所有行都被改了! 原因出在 fill 的机制:fill([]) 只创建了一个 数组对象,然后把它的引用 填入所有 7 个位置。arr[0]arr[1]......全部指向同一个数组。

正确做法

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], [], [], [], [], [], []] ← 正确!

嵌套遍历的性能细节

javascript 复制代码
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], i, j);
    }
}

两个小优化:

  • arr.length 缓存到局部变量 outerLen------避免每次循环访问对象属性。
  • 内层循环同样缓存 arr[i].length------原理相同。

引用类型与基本类型的核心区别在这里体现得淋漓尽致:基本类型 fill(1) 是值拷贝,互不影响;引用类型 fill([]) 是地址拷贝,牵一发动全身。


六、总结

  1. 数组底层 :连续内存 + ADT(存储 + 操作),原型链挂载在 Array.prototype
  2. 增删四兄弟push / pop / shift / unshift,需注意返回值(新长度 vs 被移除值),均修改原数组。
  3. 纯函数族map / filter / every / some / reduce 不修改原数组,适合数据处理流水线。
  4. 遍历抉择 :性能优先用 for,语义优先用 forEach,转换用 map,筛选用 filter,汇总用 reduce
  5. 二维数组fill([]) 是引用陷阱,必须用循环逐行创建独立数组。

一条核心直觉:数组在 JS 里既是"连续的",又是"对象的"------理解它的内存布局决定了你的代码写不写得对,理解它的方法族决定了你的代码写不写得好。


------ 数组虽基础,吃透不容易。

相关推荐
小花酱酱1 小时前
QQ群里只有你一个人?邪门歪道破局之路——AstrBot
javascript
mONESY1 小时前
前端零基础精讲:Canvas3D、CSS3D、文档流、定位全方位复盘
javascript
亿元程序员1 小时前
美术妹子让我给模型加个描边,我差点把Cocos卸了
前端
IT_陈寒2 小时前
React的useEffect依赖数组把我坑惨了,真相其实很简单
前端·人工智能·后端
徐小夕2 小时前
JitWord 3.0 正式发布,高精度Word异构解析+复杂组件兼容,打造web端协同Word编辑器
前端·vue.js·算法
恋猫de小郭2 小时前
KMP / CMP 鸿蒙版本 Beta 发布,他有什么特别之处?
android·前端·flutter
乘风gg2 小时前
OpenClaw 爆火,但”飞书"赢麻了!!!
前端·ai编程·claude
Oneslide3 小时前
React 纯前端技术栈报告(2026年)
前端