JS动态创建指定行列的二维数组

作者:青柠

时间:2026-11-06

浏览:566

深入讲解在JavaScript中动态创建指定行数和列数的二维数组的方法,剖析数组嵌套内存结构、常见Array.fill引用共享陷阱,并提供双重for循环、Array.from一行写法及高健壮性工程封装。

文章目录
主题:JavaScript 数据结构 适用环境:Node.js / 现代浏览器 (ES6+) 内容类型:核心原理与工程实操
JS动态创建指定行列的二维数组
图 1:JS 动态创建指定行列二维数组的输入规格与矩阵目标实体结构
在 JavaScript 中动态创建指定 rows(行数)与 cols(列数)的二维数组,最关键的原则是确保每一行都是独立分配的数组实例。现代开发中最推荐的标准写法是 Array.from({ length: rows }, () => new Array(cols).fill(initialValue));而在注重高频循环与底层性能的算法场景中,使用显式双重 for 循环则是最稳妥高效的基础方案。

JavaScript 二维数组的本质与内存结构

许多静态类型语言(如 C/C++ 或 Java)支持真正连续内存块的多维数组,而 JavaScript 本身并没有底层的多维数组类型。在 JavaScript 中,所谓的二维数组本质上是元素为数组的一维数组(Array of Arrays)。外层数组存储的是一组指针(引用地址),每一个指针指向堆内存中独立分配的单行数组对象。

当执行 matrix[r][c] 读取或修改数据时,JavaScript 引擎经历两个独立动作:首先通过外层索引 matrix[r] 定位到第 r 行的子数组对象,随后在该子数组内通过列索引 [c] 获取或修改单元格的值。正因如此,如果外层数组中多个槽位保存的是同一个子数组的引用地址,那么对其中任意一处 matrix[r][c] 的修改,都会同步影响到其他行。

JS动态创建指定行列的二维数组
图 2:JavaScript 二维数组在堆内存中的引用分配与坐标寻址机制

从内存结构图可以清晰看到,只有当堆内存中存在 rows 个独立的单行数组实例(例如 @0x101、@0x102、@0x103)时,二维数组的读写才是安全隔离的。任何动态创建二维数组的方法,其根本诉求都在于如何精确且高效地为每一行调用一次构造操作。

基础实现:双重 for 循环分步构建

使用传统的双重 for 循环创建二维数组,是逻辑最透明、兼容性最好且在各浏览器引擎中优化最彻底的方案。该方法完全避免了函数回调的调用栈开销与隐式行为。

执行步骤分解

  1. 初始化外层容器:声明一个空数组 const matrix = [],作为容纳所有行的主矩阵。
  2. 启动外层行遍历:通过计数器 r 从 0 遍历至 rows - 1,每次迭代代表一行。
  3. 创建独立单行实例:在每轮行循环内部,执行 const row = [] 开辟全新的单行数组对象。
  4. 内层列循环填充单元格:通过计数器 c 从 0 遍历至 cols - 1,将初始值或计算值推入 row.push(val)。
  5. 行装配并推入外层:整行装配完毕后,通过 matrix.push(row) 存入外层数组。外层循环结束后返回 matrix。
JS动态创建指定行列的二维数组
图 3:双重 for 循环动态构建指定行列二维数组的 5 个关键执行步骤

完整代码示例

/**
 * 使用双重 for 循环动态创建指定行列的二维数组
 * @param {number} rows - 行数
 * @param {number} cols - 列数
 * @param {*} initialValue - 每个单元格的初始填充值(默认为 0)
 * @returns {Array<Array>} 独立的二维数组
 */
function createMatrixByLoop(rows, cols, initialValue = 0) {
  const matrix = [];
  
  for (let r = 0; r < rows; r++) {
    const row = []; // 每行必须重新声明,开辟独立内存地址
    for (let c = 0; c < cols; c++) {
      row.push(initialValue);
    }
    matrix.push(row);
  }
  
  return matrix;
}

// 调用示例:生成 3 行 4 列,初始值全部为 0 的矩阵
const grid = createMatrixByLoop(3, 4, 0);
grid[0][1] = 99;

// 验证输出结果
console.log(grid);
/* 示例输出:
[
  [ 0, 99, 0, 0 ],
  [ 0,  0, 0, 0 ],
  [ 0,  0, 0, 0 ]
]
*/
性能优化提示:如果需要创建尺寸极大(如 10000 × 10000)的矩阵,可以通过 const matrix = new Array(rows) 和 const row = new Array(cols) 预先向 V8 引擎申请固定长度的内存空间,随后使用下标赋值 row[c] = initialValue 代替 push(),这样可以减少动态扩容带来的内存重分配耗时。

避坑指南:Array.fill 嵌套共享引用陷阱

在日常开发与面试中,初学者非常容易写出一种看似短小精悍、实则暗藏严重缺陷的写法:

// 危险写法:请勿在实际业务中使用!
const badMatrix = new Array(rows).fill(new Array(cols).fill(0));
缺陷表现:当你执行 badMatrix[0][1] = 99 试图仅修改第 0 行第 1 列时,你会震惊地发现 badMatrix[1][1] 和 badMatrix[2][1] 也全部自动变成了 99!整列数据全部被改动了。
JS动态创建指定行列的二维数组
图 4:Array.fill 共享引用陷阱与独立实例化的内存状态对比剖析

产生该 Bug 的底层机制

在 JavaScript 中,函数参数是在调用该函数之前先被计算求值的。因此,在 .fill(new Array(cols).fill(0)) 执行时:

  • 括号内部的 new Array(cols).fill(0) 仅仅只在内存中被执行创建了一次,生成了一个数组对象实体,假设其堆内存地址为 @0xERR。
  • 外层的 fill() 方法接收到这个地址值,并将外层数组的每一个槽位(从 0 到 rows - 1)都赋值为这同一个地址 @0xERR。
  • 结果就是:外层数组的所有索引元素,本质上是同一物理数组的“多个别名”。修改 badMatrix[0] 实际上就是修改 badMatrix[1] 和 badMatrix[2]。

核心法则:如果填充值是一个对象或数组(引用类型),绝不能使用 Array.prototype.fill() 进行初始化,除非你明确希望所有行共享同一组状态。

现代标准:利用 Array.from 简洁优雅实现

ECMAScript 6 (ES2015) 引入了 Array.from() 静态方法。该方法不仅可以将类数组对象转换为真正数组,还接收一个第二个参数——映射回调函数(mapFn)。这个特性为动态创建二维数组提供了目前业界最优雅、语法最简洁的解决方案。

基础语法:固定值填充

/**
 * 现代规范写法:一行代码动态创建二维数组
 */
const createMatrix = (rows, cols, fill = 0) =>
  Array.from({ length: rows }, () => new Array(cols).fill(fill));

// 生成 2 行 3 列二维数组
const m = createMatrix(2, 3, 7);
console.log(m);
// 输出:[ [ 7, 7, 7 ], [ 7, 7, 7 ] ]

// 验证行独立性
m[0][0] = 999;
console.log(m[1][0]); // 输出 7,完全独立,互不影响
为什么 Array.from 不会出现引用陷阱?
因为第二个参数 () => new Array(cols).fill(fill) 是一个箭头回调函数。在生成每一行时,Array.from 都会独立调用一次这个回调。循环进行了 rows 次,回调就执行了 rows 次,每次都返回了一个全新的 new Array(cols) 实例。

进阶应用:根据行列坐标动态计算初始值

如果矩阵的初始值并不是单一的静态常数,而是需要根据单元格的坐标位置 (r, c) 进行运算(例如坐标矩阵、渐变编号或国际象棋黑白棋盘),可以对内外层同时使用 Array.from():

// 利用内外层映射函数的索引参数 (r, c) 动态计算每个格子的初始值
const generateNumberedGrid = (rows, cols) =>
  Array.from({ length: rows }, (_, r) =>
    Array.from({ length: cols }, (_, c) => r * cols + c)
  );

const gridNumbers = generateNumberedGrid(3, 4);
console.log(gridNumbers);
/* 示例输出(连续序列编号):
[
  [ 0,  1,  2,  3 ],
  [ 4,  5,  6,  7 ],
  [ 8,  9, 10, 11 ]
]
*/

工程封装:高健壮性通用工具函数

在生产环境的代码库中,函数往往会面临非法输入或边界参数,例如传入负数、浮点数、非数字类型或者 0。因此,编写一个健壮的工具函数应当包含必要的防御性校验,并兼顾静态填充与动态工厂函数。

/**
 * 生产级通用二维数组创建工厂
 * @param {number} rows - 矩阵行数(必须为非负整数)
 * @param {number} cols - 矩阵列数(必须为非负整数)
 * @param {*|Function} [filler=null] - 填充值,若为函数则按 (row, col) 执行返回
 * @returns {Array<Array>} 二维数组实例
 */
function create2DArray(rows, cols, filler = null) {
  // 1. 参数合法性严格防守
  if (!Number.isInteger(rows) || !Number.isInteger(cols)) {
    throw new TypeError("行数(rows)与列数(cols)必须为整数");
  }
  if (rows < 0 || cols < 0) {
    throw new RangeError("行数与列数不能为负数");
  }

  // 2. 处理 0 行或 0 列的极端边界
  if (rows === 0) return [];
  if (cols === 0) {
    return Array.from({ length: rows }, () => []);
  }

  // 3. 根据 filler 是否为函数区分处理
  const isFactory = typeof filler === "function";

  return Array.from({ length: rows }, (_, r) => {
    if (!isFactory) {
      // 静态值且为原始类型时,结合 new Array().fill() 性能最优
      return new Array(cols).fill(filler);
    }
    // 动态工厂生成
    return Array.from({ length: cols }, (_, c) => filler(r, c));
  });
}

// 测试用例 1:快速生成 2x3 布尔矩阵
const boolMatrix = create2DArray(2, 3, false);

// 测试用例 2:生成每个单元格为独立对象的数据表
const cellMatrix = create2DArray(2, 2, (r, c) => ({
  id: `node-${r}-${c}`,
  active: false
}));

方案选型与技术总结

在实际项目架构中,面对不同规模的数据集和业务场景,开发者应选用最契合具体需求的技术路径:

JS动态创建指定行列的二维数组
图 5:JavaScript 动态创建二维数组的方案选型路径与能力指标横评
实现方案 核心代码范例 优点 局限与注意事项 最适场景
Array.from 映射 Array.from({length: r}, () => Array(c).fill(v)) 代码极其精简,可读性强,语义明确,天然规避引用共享 存在外层函数调用开销,超高频迭代下略慢于原生循环 日常业务逻辑、前端页面表格、算法面试、中小规格矩阵(推荐首选)
双重 for 循环 for(let r=0;...) { for(let c=0;...) } 执行性能最强,无多余调用栈开销,底层引擎 JIT 优化极高 代码结构冗长,需要 6~8 行代码完成 超大规模矩阵(10000+ 规格)、实时图形变换、物理模拟运算
一维 TypedArray 模拟 new Float64Array(r * c)
通过 r * cols + c 寻址
连续内存空间,零对象引用开销,没有垃圾回收负担 无法直接使用 [r][c] 语法,必须封装一维下标转换公式 WebGL、音频合成、图像像素处理、科学矩阵计算

总结而言:在 90% 以上的前端与 Node.js 业务开发中,直接使用 Array.from() 方案是兼顾开发效率与运行可靠性的最佳选择;牢记不要将引用对象直接作为 Array.prototype.fill() 的参数,即可从根本上杜绝跨行数据污染的隐蔽 Bug。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章