freeCodeCamp 基础 JavaScript 挑战精讲:用多维数组构建 Shopping List 购物清单
本文围绕 freeCodeCamp 开源仓库中 Basic JavaScript(基础 JavaScript)块内的 Shopping List 编程挑战展开,逐句拆解题目要求、参考解法与自动化测试逻辑,并回到仓库源码与相邻课程文件,弄清"多维数组(multi-dimensional array)"这一核心概念在真实课程体系中的前因后果。读完本文,你将能独立完成该挑战,并掌握用
[名称, 数量]这样的成对数组结构建模数据的通用思路。
挑战出处:Basic JavaScript 块末尾的一道综合练习
在 freeCodeCamp 的课程仓库中,所有课程都被组织为 Superblock → Block → Challenge 三层结构,每个挑战是一个独立的 Markdown 文件。本次的 Shopping List 挑战位于:
从 basic-javascript 块元数据 可以看到,56533eb9ac21ba0edf2244bc(即 Shopping List)在 challengeOrder 中紧跟在 push / pop / shift / unshift 四个数组操作课程之后、Write Reusable JavaScript with Functions(函数入门)之前,是一道典型的学完新知识后进行综合应用的过渡练习;而该块的 blockLayout 被标记为 legacy-challenge-list。整个 basic-javascript 块又是仓库 JavaScript 算法与数据结构 superblock 结构文件 中排在首位(blocks[0])的课程块,其后依次是 es6、regular-expressions、debugging、basic-data-structures 等,构成了完整的学习主线。
每个挑战文件顶部的 frontmatter 记录了元信息,Shopping List 的结构是:
id: 56533eb9ac21ba0edf2244bc
title: Shopping List
challengeType: 1
forumTopicId: 18280
dashedName: shopping-list
其中 id 是全局唯一标识(同一文件在本仓库的块顺序、链接跳转中都依赖它),dashedName 是 URL 友好形式的课程名。正文则按 --description--(题目描述)、--hints--(自动化测试断言)、--seed--(初始代码)、--solutions--(参考解法)四段组织,这也是理解 freeCodeCamp "课程即代码"理念的入口。
题目拆解:把一段购物需求翻译成数据结构
挑战原文给出的任务非常简短:
Create a shopping list in the variable
myList. The list should be a multi-dimensional array containing several sub-arrays. The first element in each sub-array should contain a string with the name of the item. The second element should be a number representing the quantity.
也就是说,需要定义一个名为 myList 的多维数组,其中每个元素又是一个子数组,每个子数组恰好两个位置:
- 第 0 位(下标 0):字符串,代表商品名称,例如
"Chocolate Bar"; - 第 1 位(下标 1):数字,代表购买数量,例如
15。
原文给出了单个条目的示例形态:
["Chocolate Bar", 15]
并要求整个列表至少包含 5 个子数组("There should be at least 5 sub-arrays in the list")。这本质上是在让你用 JS 原生数据结构表达一张"商品名 + 数量"的二维表格:外层数组是清单本身,内层两元素数组是清单行。
编辑器初始给你的是这样一行种子代码:
const myList = [];
注意种子使用 const 声明而非 var——即使是在 legacy 块中,freeCodeCamp 也默认引导学习者使用块级常量声明;此处 myList 的引用不会被重新赋值,因此 const 完全够用。
前置知识复习:从一维数组到"数组的数组"
要解出这道题,需要先弄清它之前几节课铺垫的三个知识点,仓库中的相邻挑战文件恰好提供了最权威的示例。
第一,数组用方括号字面量存储多个值。 在 Store Multiple Values in one Variable using JavaScript Arrays 中给出的标准写法是:
const sandwich = ["peanut butter", "jelly", "bread"];
这条课程还特别验证了 JS 数组是可异构的容器:同一数组可以混装字符串和数字。
第二,数组可以嵌套在数组里,形成多维数组。 Nest one Array within Another Array 明确定义了术语:teams = [["Bulls", 23], ["White Sox", 45]] 这种形态被称为 multi-dimensional array(多维数组),即"数组的数组"。
第三,用连续的多对方括号逐层取值。 Access Multi-Dimensional Arrays With Indexes 给出了行/列式的取值心智模型:
const arr = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[[10, 11, 12], 13, 14]
];
const subarray = arr[3]; // [[10, 11, 12], 13, 14]
const nestedSubarray = arr[3][0]; // [10, 11, 12]
const element = arr[3][0][1]; // 11
第一对方括号定位最外层,之后每多一对方括号就向下深入一层。该课程还给出了一条书写规范提醒:数组名与方括号之间不要加空格,arr[0][0] 可以,arr [0] [0] 虽能被 JS 解析但不推荐。
Shopping List 正是这三节课知识点的收口应用:既要知道子数组形态怎么写,又要理解外层数组把多个条目装在一起的含义。
参考实现:一行行看懂官方答案
仓库里随挑战保存的官方参考解法(见挑战文件末尾的 --solutions-- 段)是:
const myList = [
["Candy", 10],
["Potatoes", 12],
["Eggs", 12],
["Catfood", 1],
["Toads", 9]
];
逐条核对:
- 外层
myList是一个数组,.length为 5,满足"至少 5 个子数组"; - 每个内层子数组恰好两个元素,第 0 位是字符串(
"Candy"等),第 1 位是数字(10、12等); - 5 行相互之间用逗号分隔,最后一行后不加逗号也完全合法。
除了这种一次性字面量写法,也可以先建空数组再用 push 动态追加,效果等价(push 正是本块前一道课程 Manipulate Arrays With push Method 的内容):
const myList = [];
myList.push(["Candy", 10]);
myList.push(["Potatoes", 12]);
myList.push(["Eggs", 12]);
myList.push(["Catfood", 1]);
myList.push(["Toads", 9]);
日常开发中,"先定义再逐行 push"常用于数据来源不确定、需按条件分批追加的场景;而挑战场景下直接写数组字面量最直观、最不易出错。
隐藏关卡:--hints-- 里的四条自动化断言
Shopping List 的"判题标准"完整写在挑战文件的 --hints-- 段里,弄懂它们等于弄懂了题目的全部隐藏约束:
① myList 必须真的是数组:
assert(Array.isArray(myList));
用 Array.isArray 而不是 typeof,是因为 typeof [] 会返回 "object",无法与普通对象区分,这正是 Store Multiple Values 挑战 中不得已用 typeof myArray == 'object' 的原因;到了 Shopping List 阶段,课程刻意改用更严谨的 Array.isArray。
② 每个子数组的第 0 位必须是字符串:
assert(myList.every(elem => elem && typeof elem[0] === 'string'));
这里 elem && 的前置守卫值得注意:如果某个元素是 null、undefined 或空数组,先短路为假值使断言通过失败判定,避免直接对空值访问 [0] 抛错。every 要求所有子数组都满足条件。
③ 每个子数组的第 1 位必须是数字:
assert(myList.every(elem => elem && typeof elem[1] === 'number'));
与上一条对称,强制了"数量必须是 number"这一语义约束,"10"(字符串数字)在此会被判错。
④ 至少要有 5 个条目:
assert(myList.length > 4);
等价于"长度必须大于 4",即至少 5 个子数组。注意它是针对外层数组长度的断言,与内层子数组个数直接相关。
综合四条断言,一个合法的 myList 必须是:外层长度 ≥ 5 的数组,且每个内层元素都是长度 ≥ 2、元素 0 为字符串、元素 1 为数字的子数组。 这正是题目描述的机器可校验版本,也体现了 freeCodeCamp 把"自然语言需求 + 可执行断言"打包在同一 Markdown 文件的课程工程化思路。
常见误区与自查技巧
- 条目不足 5 个:
length为 3 或 4 会直接触发第 ④ 条断言失败。数清楚子数组用逗号分隔的个数即可。 - 字符串与数字写反:如
[10, "Candy"],会同时触发第 ②③ 条失败。始终记住约定顺序是"名称在左(下标 0)、数量在右(下标 1)"。 - 多包了一层或漏了一层:写成
myList = ["Candy", 10]则外层元素不是数组,every中elem[0]会取到字符"C"(仍可能是 string)但第二条对elem[1]就会取到undefined;而写成[[["Candy"], 10]]这类过度嵌套虽然不会全部报错,却偏离了"两元素子数组"的数据契约。 - 误用对象代替数组:
{ item: "Candy", qty: 10 }这类对象字面量不符合子数组结构,会被第 ② 条拒绝。
自查时可以在浏览器控制台或 Node REPL 里快速验证:
console.log(Array.isArray(myList)); // true
console.log(myList.length); // >= 5
console.log(myList.every(e => e && typeof e[0] === 'string' && typeof e[1] === 'number')); // true
延伸:这一模式在后续课程里反复出现
[label, value] 这种"成对数组"结构在本块乃至后续块中被反复使用:
- 紧随其后的
Accessing Nested Arrays、Nesting For Loops会要求你用双层循环或连续下标遍历二维结构; - 更后面的
es6、basic-data-structures等块(同见 superblock 结构文件)则会引入Map、解构赋值等更高级的键值容器——购物清单挑战是理解它们之前的"最小可行数据模型"。
真正掌握这道题的标准是:看到"若干行、每行两个字段"的数据,能下意识地用 [[a, b], [c, d], ...] 建模,并能用 myList[row][col] 读取任意格子。
如何在仓库中复现与继续深挖
本仓库就是 freeCodeCamp 的开源代码与课程库,所有课程文件均以 Markdown + YAML frontmatter 形式存放,可以按以下路径研读:
- 挑战原文、seed 与官方解法:curriculum/challenges/english/blocks/basic-javascript/56533eb9ac21ba0edf2244bc.md
- 本块全部挑战的标题顺序与 id:curriculum/structure/blocks/basic-javascript.json
- 所属 superblock 的块级编排:curriculum/structure/superblocks/javascript-algorithms-and-data-structures.json
- 数组入门的相邻课程:一维数组存储多值、数组嵌套数组、用下标访问多维数组
此外,从仓库结构看,curriculum/schema 目录下维护着挑战与课程结构的 schema 定义及配套校验测试(如 challenge-schema.js、curriculum-schema.js),如果你希望深入理解挑战文件格式如何被机器校验,这是很好的下一站;curriculum/src/test 下也沉淀了课程级测试工具链。总而言之,Shopping List 虽是一道入门练习,但它完整承载了"题目描述—初始代码—测试断言—官方解法"四段式课程文件的结构,吃透它,你既能收获多维数组的实战手感,也掌握了继续阅读 freeCodeCamp 数千道课程文件的格式钥匙。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00