首页
/ freeCodeCamp 课程实战解析:用 JavaScript 构造复杂的多维嵌套数组(Basic Data Structures)

freeCodeCamp 课程实战解析:用 JavaScript 构造复杂的多维嵌套数组(Basic Data Structures)

2026-09-06 18:38:31作者:邵娇湘

多维(嵌套)数组是 freeCodeCamp「Basic Data Structures」课程中承上启下的核心练习:它把前面学到的方括号访问、各类数组方法与数据结构意识整合起来,要求读者亲手构造一个深达 5 层的嵌套数组。本文以该挑战的官方题面文件为主线,逐层拆解嵌套数组的概念、深度计数、方括号读写技巧,并结合仓库内真实的课程配置与测试脚本,说明这类练习题的评判原理,帮助你既能独立完成挑战,也能理解挑战背后的 JavaScript 递归逻辑。

一、挑战在课程中的位置与题面文件

本挑战文件位于课程源码目录:

curriculum/challenges/english/blocks/basic-data-structures/587d7b7b367417b2b2512b16.md

文件头部的 frontmatter 记录了它的元信息:

  • id: 587d7b7b367417b2b2512b16:全仓库唯一的挑战标识;
  • challengeType: 1:表示这是一道 JavaScript 编写代码挑战
  • forumTopicId: 301159:供论坛讨论串使用的编号;
  • dashedName: create-complex-multi-dimensional-arrays:用于生成 URL 的小写连字符标题。

其中 challengeType 的定义集中在共享配置 packages/shared/src/config/challenge-types.tsconst js = 1const html = 0 并列;同文件第 107-108 行又将这类挑战的视图类型映射为 'classic',即典型的「左侧题面、右侧代码编辑器 + 运行测试」经典界面。

从区块顺序配置文件 curriculum/structure/blocks/basic-data-structures.json 可以看到,本挑战排在该区块的第 12 位——前面依次是数组存取、push/pop/shift/unshiftsplice/slice、展开运算符、indexOffor 循环遍历,紧接着便是面向对象「键值对」系列的练习。换句话说,它是数组部分的收官题,用一层更复杂的嵌套来检验前面所有技能的掌握程度。

二、核心概念:什么是多维(嵌套)数组

数组作为数据结构最强大的特性之一是:数组中可以存放其他数组,甚至可以完全由数组构成。当数组内部又包含数组、且嵌套可以层层延续时,就构成了所谓的 multi-dimensional(多维)或 nested(嵌套)数组。JS 中这种嵌套没有官方层面的深度上限——数组内部可以是任意深度的数组,每个子数组又可以有各自任意的层级,于是数组可以在非常短的字面量内演变成相当复杂的数据结构。

题面给出的示例(见原文 description 小节):

let nestedArray = [
  ['deep'],
  [
    ['deeper'], ['deeper']
  ],
  [
    [
      ['deepest'], ['deepest']
    ],
    [
      [
        ['deepest-est?']
      ]
    ]
  ]
];

这段代码的层级关系可以这样理解:数组字面量的每一层 [ ] 都代表一层嵌套。以最外层数组为起点向内数:

  • ['deep'] 里的字符串处于第 2 层;
  • 第二行里 ['deeper'] 的字符串处于第 3 层;
  • 第三行里 ['deepest'] 的字符串处于第 4 层;
  • 最内侧的 ['deepest-est?'] 字符串则深达第 5 层。

现实数据虽然极少故意写得如此「绕」,但当面对大量由程序生成的、结构高度雷同的数据(例如目录树、嵌套 JSON、网格矩阵的层级化描述)时,这种多层级结构并不罕见。

用方括号符号直达最深层

无论数组嵌套多深,访问方式始终是同一个:从左到右逐层给出下标。题面示例(文档 L36-L40):

console.log(nestedArray[2][1][0][0][0]); // "deepest-est?"

逐层解读这条下标链:

  1. nestedArray[2] → 取出第三个大元素,即包含两层嵌套的区块;
  2. [1] → 取出其中第二组 [ [...['deepest-est?']] ]
  3. [0] → 进入唯一的内层数组;
  4. [0] → 再进入一层;
  5. [0] → 终于取到字符串 deepest-est?

深层数据同样可以重新赋值

既然能定位到最深层,自然也能原地改写它(文档 L42-L50):

nestedArray[2][1][0][0][0] = 'deeper still';

console.log(nestedArray[2][1][0][0][0]); // 现在输出 "deeper still"

赋值语句的左侧与读取时写法完全一致,方括号符号天然具有「左值」能力,这正是后续用嵌套数组承载复杂数据时更新局部信息的基础。

三、本次挑战任务逐条解读

题面要求(instructions 小节)非常具体:

已定义变量 myNestedArray 并赋值为一个数组。请修改 myNestedArray,数据元素只能使用字符串、数字、布尔值,使整个结构恰好有五层深度(记住:最外层数组是第 1 层);并保证在第 3 层某处包含字符串 deep,第 4 层包含 deeper,第 5 层包含 deepest

初始 seed 代码(seed-contents 小节)如下:

let myNestedArray = [
  // Only change code below this line
  ['unshift', false, 1, 2, 3, 'complex', 'nested'],
  ['loop', 'shift', 6, 7, 1000, 'method'],
  ['concat', false, true, 'spread', 'array'],
  ['mutate', 1327.98, 'splice', 'slice', 'push'],
  ['iterate', 1.3849, 7, '8.4876', 'arbitrary', 'depth']
  // Only change code above this line
];

可以留意两个细节:

  1. 代码编辑区域被 // Only change code below this line// Only change code above this line 两条注释框定,评测系统关心的是最终变量值,而不是改动范围——你既可以只在标记行之间嵌入嵌套结构,也可以整段重写;
  2. 种子数据里的五个一维子数组分别以常见数组方法命名(unshiftshiftconcatmutateiterate),相当于对前面挑战做过的方法名做了一次「目录式」回顾,此刻这些方法名本身只是普通字符串元素。

关于“深度”的可靠口径

挑战的核心难点在于对「第几层」的理解。题面特别强调 the outer-most array is level 1(最外层数组是第 1 层),也就是说:五层深度 = 最外层数组本身(第 1 层)+ 其内部再套 4 层括号链。直观地说,某个字符串前面出现几对成层嵌套的 [ ],它就位于第几层——字符串 deepdeeperdeepest 必须分别落在第 3、4、5 层的括号包裹中。

四、读懂自动测试:四个断言到底在检查什么

freeCodeCamp 的每道挑战都会附带若干断言(hints),它们以纯 JavaScript 写成、由评测系统在浏览器中执行。理解这些断言 = 理解「达标」的精确定义。本挑战的四个断言(见 hints 小节)值得逐条分析,它们本身就是很好的递归学习材料。

断言一:叶子元素只能是 number / string / boolean

第一个断言先用递归展平把嵌套数组拍平成一位数组:

let flattened = (function flatten(arr) {
  const flat = [].concat(...arr);
  return flat.some(Array.isArray) ? flatten(flat) : flat;
})(arr);

实现思路是经典的递归扁平化:每次用展开运算符 ... 把当前层所有元素取出并 concat 到新数组;只要新数组里还存在数组元素(flat.some(Array.isArray)),就继续递归展开,直到全部是标量为止。随后遍历展平结果,逐一用 typeof 检查类型,只要出现 numberstringboolean 之外的类型就返回 false。这相当于声明:嵌套只能通过「数组里套数组」实现,不得引入对象、函数或 undefined 等作为叶子

断言二:最深层数组的递归深度恰好为 4

第二个断言递归统计最深非数组元素所处的深度:

function arrayDepth(array, i, d) {
  if (Array.isArray(array[i])) {
    arrayDepth(array[i], 0, d + 1);
  } else {
    depth = d > depth ? d : depth;
  }
  if (i < array.length) {
    arrayDepth(array, i + 1, d);
  }
}
arrayDepth(arr, 0, 0);
return depth; // 期望 === 4

这里的关键是计数基准:递归从 d = 0 开始,最外层数组本身不计入,每向下一层 d + 1。因此断言期望值是 4 而不是 5——换算成题面的人话就是「最外层算第 1 层时共 5 层」。「测试期望 4」与「题面要求 5 层」并不矛盾,只是递归脚本把根数组记作第 0 层、根数组内容算作第 1 层而已。

断言三到五:指定字符串必须出现在指定深度、且只出现一次

后面三个断言共用同一个工具函数 howDeep,它递归收集某个目标字符串出现的所有深度:

function howDeep(array, target, depth = 0) {
  return array.reduce((combined, current) => {
    if (Array.isArray(current)) {
      return combined.concat(howDeep(current, target, depth + 1));
    } else if (current === target) {
      return combined.concat(depth);
    } else {
      return combined;
    }
  }, []);
}

它用 reduce 遍历当前层的每个元素:元素是数组就带着 depth + 1 递归下去;元素严格等于 target 就把当前 depth 记录进结果。三个断言分别要求:

  • 字符串 deep 的出现次数为 1,且出现位置(0 起算的递归深度)为 2——即第 3 层;
  • 字符串 deeper 出现次数为 1,深度为 3——即第 4 层;
  • 字符串 deepest 出现次数为 1,深度为 4——即第 5 层。

两个条件通过 && 连在一起:既要「恰好出现一次」,又要「出现在正确深度」。这意味着把三个词多放、少放、放错层,都无法通过测试。值得注意的是这三个断言只关注 deep/deeper/deepest 三个词与整体最大深度,其余数据元素的排布是自由的——这给了你相当大的设计空间。

五、参考答案逐层拆解

官方 solution(solutions 小节):

let myNestedArray = [
  ['unshift', ['deep', ['deeper', ['deepest']]], false, 1, 2, 3, 'complex', 'nested'],
  ['loop', 'shift', 6, 7, 1000, 'method'],
  ['concat', false, true, 'spread', 'array'],
  ['mutate', 1327.98, 'splice', 'slice', 'push'],
  ['iterate', 1.3849, 7, '8.4876', 'arbitrary', 'depth']
];

改动点全部集中在第一个子数组中:把原来的字符串元素 'nested' 之前插入了一整条嵌套链 ['deep', ['deeper', ['deepest']]]。下面把该链在整数组中的层级逐层标注出来:

取值路径(从最外层起) 得到的内容 该内容所在层级
myNestedArray 整个数组 第 1 层(最外层)
myNestedArray[0] 第一个子数组 第 2 层
myNestedArray[0][1] ['deep', ['deeper', ['deepest']]] 第 3 层
myNestedArray[0][1][0] 'deep'(位于第 3 层内部) 第 3 层元素
myNestedArray[0][1][1] ['deeper', ['deepest']] 第 4 层
myNestedArray[0][1][1][0] 'deeper' 第 4 层元素
myNestedArray[0][1][1][1] ['deepest'] 第 5 层
myNestedArray[0][1][1][1][0] 'deepest' 第 5 层元素

对应关系一目了然:deep 在 3 层、deeper 在 4 层、deepest 在 5 层,且各只出现一次;其余元素仍是字符串、数字、布尔值,满足全部断言。这个答案巧妙地借用了原本就存在的第一行数组来容纳嵌套链,其余四行完全不动。

其他同样能通过的写法

因为测试只约束类型、最大深度与三个关键词的位置,你完全可以用更「对称」的写法,例如把三关键词放到第三、四行子数组的链上,或在第一行内部追加更多层级结构。下面是一种把嵌套集中在一个子元素的等价写法:

let myNestedArray = [
  ['unshift', false, 1, 2, 3, 'complex', ['nested',
    ['deep', ['deeper', ['deepest']]]
  ]],
  ['loop', 'shift', 6, 7, 1000, 'method'],
  ['concat', false, true, 'spread', 'array'],
  ['mutate', 1327.98, 'splice', 'slice', 'push'],
  ['iterate', 1.3849, 7, '8.4876', 'arbitrary', 'depth']
];

无论哪种写法,完成后都可以自行验证:把 myNestedArray[0][1][1][1][0] 等路径打印出来,确认与设计层级一致。

六、常见误区与调试技巧

综合题面与上述递归断言,动手时最容易踩的坑有以下几类:

  1. 对「最外层即第 1 层」理解偏差。常见做法是只嵌套了 3~4 层括号链就以为到了第 5 层。校验时记住:最大深度断言的递归基准是根数组深度 0、期望 4,所以你在字面量里必须形成「1 个根数组 + 4 层内嵌数组」共 5 层括号嵌套的链条。
  2. 把关键词放错层。三个字符串必须分别出现在 0 起算深度 2 / 3 / 4 的位置,简单起见可按参考答案的结构逐级对齐路径再核对下标个数。
  3. 叶子类型不合法。若在某层塞入对象 {}null、函数等,会直接触发第一个断言失败。想构造更复杂结构可以等学习对象之后再进行——题面也明确提示,在转向 Object 之前这是最后一次数组「加难度」的机会。
  4. 某个关键词出现多次。每个断言都校验 length === 1,重复放置会让对应断言失效。种子数据本身就是已符合类型要求的数组,改动时尽量增量修改而非大范围重写,可减少无意中复制关键词的风险。

排查时最实用的手段是在编辑器中逐层 console.log 关键路径的下标,例如对参考答案输出 myNestedArray[0][1]myNestedArray[0][1][1],确认每一层解包出来的内容符合预期,再点 Run the Tests。

七、挑战背后的文件结构与开源可验证性

如果你对这类 Markdown 挑战文件本身如何「变成」可运行练习感兴趣,可以继续在仓库中探索:每个挑战文件都被 tools/challenge-parser 目录下的解析器处理,把 --description----instructions----hints----seed----solutions-- 等分隔区解析为结构化数据(对应插件见 tools/challenge-parser/parser/plugins/add-seed.js 等),其中 --solutions-- 会被收集为参考实现。而挑战结构(如题目顺序、所属帮助分类)则由 curriculum/structure/blocks/basic-data-structures.json 这类区块配置文件声明,并通过 curriculum/schema/challenge-schema.js 中的 Joi schema 校验字段合法性。因此你在编辑器里点「Run the Tests」看到的每一个断言,都能在本仓库的源码中被精确定位与复现。

八、小结

本挑战表面上是「造一个更深的数组」,实质是对三层能力的综合检验:一是对方括号符号逐层定位的熟练度,二是对数组嵌套即数据结构层级模型的直观理解,三是读懂递归判定逻辑(展平、统计深度、收集目标出现位置)的能力。掌握它之后,后续课程会正式进入 JavaScript Object 的世界——届时你会看到数组与对象互相嵌套的「数组对象混合结构」,那正是几乎所有真实 Web 数据(如 JSON)的基本形态。本文提供的层级换算方法、逐断言解读与可运行的参考实现,足以让你在编辑器中独立构造出任意的多维结构并自信地通过全部测试。

登录后查看全文
热门项目推荐
相关项目推荐