freeCodeCamp JavaScript 每日挑战深度解析:Challenge 21 Hex Generator 随机主导色生成
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本篇围绕 freeCodeCamp 课程仓库中的 JavaScript 每日编程挑战第 21 题「Hex Generator(十六进制色值生成器)」展开:完整继承原题的题目描述、验收测试与参考解答,并结合该挑战在课程块结构与平台测试运行时的配置,讲清「如何生成一个指定颜色通道占主导的随机六位十六进制色值」的实现思路、边界条件与常见陷阱,读完后你可以直接复现该题的完整解法,并理解其随机范围设计背后的数学约束。
题目定位:它属于哪一类挑战
这道挑战的源文件位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/6821ebf3237de8297eaee797.md,front-matter 中的关键字段为:
id: 6821ebf3237de8297eaee797 title: "Challenge 21: Hex Generator" challengeType: 28 dashedName: challenge-21其中challengeType: 28并非随意编号。在共享配置 packages/shared/src/config/challenge-types.ts 中可以确认其语义:
const dailyChallengeJs = 28;并且该类型在配置中被归入classic编辑器布局(第 134 行[dailyChallengeJs]: 'classic'),测试展示方式为tests(第 175 行[dailyChallengeJs]: 'tests')。也就是说,这是 freeCodeCamp 的JavaScript 每日挑战(Daily Coding Challenge)类型,运行在经典单文件代码编辑器中,通过 Chai 风格断言(assert.equal、assert.isTrue、assert.isAbove等)验证你的实现。
从课程块结构看,该挑战的编排定义在 curriculum/structure/blocks/daily-coding-challenges-javascript.json 中,块配置包含:
usesMultifileEditor: true:块级启用多文件编辑器能力;helpCategory: "JavaScript":求助分类归属 JavaScript;blockLayout: "legacy-challenge-list":以传统挑战列表方式呈现;disableLoopProtectTests: true:禁用循环保护测试。
在challengeOrder数组中,本挑战的 id6821ebf3237de8297eaee797排在 Challenge 20(Array Duplicates)之后、Challenge 22(Tribonacci Sequence)之前,正是第 21 位。值得一提的是,与本题主题最接近的两道题目也都在同一个块里:Challenge 23「RGB to Hex」(6821ebfd237de8297eaee799.md)和 Challenge 62「Hex to Decimal」,构成了一条「颜色与十六进制」的练习线。
题目要求:完整继承原始描述
原题的完整题目描述如下:
Given a named CSS color string, generate a random hexadecimal (hex) color code that is dominant in the given color.
- The function should handle
"red","green", or"blue"as an input argument.- If the input is not one of those, the function should return
"Invalid color".- The function should return a random six-character hex color code where the input color value is greater than any of the others.
- Example of valid outputs for a given input:
| Input | Output |
|---|---|
"red" | "FF0000" |
"green" | "00FF00" |
"blue" | "0000FF" |
把要求拆解成可验证的约束:
- 输入域:只接受三个命名色字符串
"red"、"green"、"blue",其他任何输入(包括大小写不同的"Red"、空串、数字)都必须走兜底分支; - 非法输入:返回字符串字面量
"Invalid color",而不是抛出异常或返回undefined; - 输出格式:六位十六进制字符,表示 RGB 三个通道;
- 主导性约束:输入颜色对应的通道值必须严格大于另外两个通道(greater than any of the others),这比「大于或等于」更苛刻——两个弱通道相等、而主导通道更大的情况才是安全的;
- 随机性约束:连续两次调用应当产生不同的色值(后面的测试会验证这一点)。
初始种子代码(即你在编辑器中看到的起点)是一个什么都不做的桩函数:
function generateHex(color) { return color; }验收测试:断言如何定义「正确」
原题--hints--部分给出了 5 组 Chai 断言测试,它们是本题验收标准的权威定义。下面逐条解读其技术含义。
1. 非法输入直接短路
assert.equal(generateHex("yellow"), "Invalid color");"yellow"不在合法集合中,函数必须返回"Invalid color"。这要求你在做任何随机数计算之前先做输入校验,否则即使后续逻辑正确也会在第一条测试上失败。
2. 长度与字符集校验
assert.lengthOf(generateHex("red"), 6);const hex = generateHex("red").toUpperCase(); const isValidHex = /^[0-9A-F]{6}$/.test(hex); assert.isTrue(isValidHex);两条测试合起来约束了输出形状:长度恰为 6,且归一化到toUpperCase()后只允许大写字母数字字符0-9A-F。这里有两个容易踩的坑:
- 测试对返回值做了
toUpperCase()才进行正则匹配,说明你的实现返回大写或小写都能通过,正则[0-9A-F]{6}本身不区分你产出的原始大小写; - 如果你返回了带
#前缀的字符串(CSS 习惯写法),lengthOf会是 7,直接失败。本题要的是不带#的裸六位色值,这与同块 Challenge 23「RGB to Hex」(要求返回#加六位小写)是两种相反的约定,不要混用。
3. 主导通道必须严格最大(以 red 为例)
const hex = generateHex("red").toUpperCase(); const isValidHex = /^[0-9A-F]{6}$/.test(hex); assert.isTrue(isValidHex); const r = parseInt(hex.slice(0, 2), 16); const g = parseInt(hex.slice(2, 4), 16); const b = parseInt(hex.slice(4, 6), 16); assert.isAbove(r, g); assert.isAbove(r, b);这是本题的核心数学约束。测试把六位字符串按每两位切片,用parseInt(..., 16)解析出三个 0–255 的十进制通道值,然后断言r > g且r > b(assert.isAbove是严格大于)。green和blue两组测试结构完全相同,只是把主导断言换成g > r && g > b和b > r && b > g。
4. 两次调用必须不同(随机性)
const hex1 = generateHex("red").toUpperCase(); // ... 解析 r1/g1/b1,断言 r1 > g1 且 r1 > b1 const hex2 = generateHex("red").toUpperCase(); // ... 解析 r2/g2/b2,断言 r2 > g2 且 r2 > b2 assert.notEqual(hex1, hex2);这组测试同时要求:两次结果都满足主导性约束,且两个色值不相等。这意味着:
- 硬编码返回
"FF0000"这类固定值能通过前三条,但会在notEqual上失败; - 如果两次调用可能随机命中完全相同的
(r, g, b)组合,理论上也会偶发失败(虽然概率极低,但随机范围设计得越窄,碰撞概率越高,见下文解析)。
参考解答:逐行剖析
原题--solutions--提供的官方参考实现:
function generateHex(color) { const toHex = n => n.toString(16).padStart(2, "0").toUpperCase(); const dominant = Math.floor(Math.random() * 86) + 170; const weak1 = Math.floor(Math.random() * 170); const weak2 = Math.floor(Math.random() * 170); let r, g, b; switch (color) { case "red": r = dominant; g = weak1; b = weak2; break; case "green": r = weak1; g = dominant; b = weak2; break; case "blue": r = weak1; g = weak2; b = dominant; break; default: return "Invalid color"; } return `${toHex(r)}${toHex(g)}${toHex(b)}`; }十六进制转换工具toHex
const toHex = n => n.toString(16).padStart(2, "0").toUpperCase();n.toString(16)把十进制数转成十六进制字符串,padStart(2, "0")保证不足两位时左补零(例如9 → "09"、15 → "0f"),toUpperCase()统一为大写("0f" → "0F")。三者合起来把 0–255 的十进制值稳定映射为两位大写十六进制字符。注意本题的测试对大小写不敏感,toUpperCase()在这里是风格选择;但如果换成大小写敏感的校验题(如同块的 Hex Validator 类题目),大小写约定就必须与题目一致。
随机范围设计:为什么是 170 和 86
const dominant = Math.floor(Math.random() * 86) + 170; // [170, 255] const weak1 = Math.floor(Math.random() * 170); // [0, 169] const weak2 = Math.floor(Math.random() * 170); // [0, 169]Math.random()返回[0, 1)的浮点数,Math.floor(Math.random() * N)因此落在整数区间[0, N-1]:
| 表达式 | 取值范围 | 作用 |
|---|---|---|
Math.floor(Math.random() * 86) + 170 | 170 … 255 | 主导通道 |
Math.floor(Math.random() * 170) | 0 … 169 | 两个弱通道 |
关键约束是dominant ≥ 170而weak ≤ 169,所以无论随机结果如何,恒有dominant > weak1且dominant > weak2。这就是主导性约束assert.isAbove(r, g)与assert.isAbove(r, b)永远成立的数学保证——解法没有在事后校验或重试,而是在采样范围上直接构造出必然成立的偏序关系。
为什么下界取 170 而不是 255?如果把主导通道固定为255(纯红FF),输出多样性只剩两个弱通道;取[170, 255]让主导通道本身也有 86 种可能,而弱通道各 170 种,三者独立采样后单次调用的完整组合空间为 86 × 170 × 170 = 2,495,800 种,notEqual(hex1, hex2)的碰撞概率约为 1/2,495,800,实践中可以认为两次调用必然不同。
switch 分发与默认分支
switch (color)把「主导值放在哪个位置」按输入分发:"red"时r = dominant,"green"时g = dominant,"blue"时b = dominant,其余任何值走default: return "Invalid color"。这个顺序与题目要求一致——先校验输入,再计算随机值(虽然参考实现是先算随机数再 switch,但default分支在拼接前就返回了,效果相同)。
最后用模板字符串把三段两位十六进制拼成六位结果:
return `${toHex(r)}${toHex(g)}${toHex(b)}`;padStart保证了每段恒为两位,所以拼接结果恒为六位字符串,assert.lengthOf(..., 6)自然满足。
常见错误与等价实现
结合上述测试约束,列出几类典型错误写法:
- 忘记处理非法输入:直接假设输入合法,
generateHex("yellow")返回了随机色值而非"Invalid color",第一条测试即失败。 - 返回带
#的字符串:"#ff0000"长度为 7,assert.lengthOf失败。 - 主导通道与弱通道范围重叠:例如三个通道都取
Math.floor(Math.random() * 256),只有约 1/6 的概率恰好满足严格主导,测试会大概率失败。 - 两个弱通道与主导通道可能相等:如果弱通道范围取
[0, 255],弱通道可能采样到与主导通道相同的值,assert.isAbove严格要求大于,相等即失败。参考解法把弱通道上限压到 169、主导通道下限抬到 170,正是从范围上排除这种碰撞。 - 固定返回值:硬编码
"FF0000"能通过格式与主导性测试,但两次调用结果相同,assert.notEqual(hex1, hex2)失败。
如果你更喜欢声明式风格,下面这个等价实现用「先排通道再分配」的思路完成同样的约束(同样满足全部断言):
function generateHex(color) { const channels = { red: [2, 1, 3], green: [1, 2, 3], blue: [1, 3, 2] }; const order = channels[color]; if (!order) return "Invalid color"; const toHex = n => n.toString(16).padStart(2, "0").toUpperCase(); const values = [ Math.floor(Math.random() * 86) + 170, // 主导:170–255 Math.floor(Math.random() * 170), // 弱:0–169 Math.floor(Math.random() * 170) // 弱:0–169 ]; return order.map(i => toHex(values[i - 1])).join(""); }其正确性与官方解答相同:主导值域[170, 255]与弱值域[0, 169]不相交,严格主导关系恒成立;order数组按 RGB 顺序取出「该位置应放主导还是弱通道」。
小结与延伸阅读
- 本题的核心不是「怎么把数字转成十六进制」,而是如何在随机采样阶段就构造出必然满足偏序约束的结果:主导通道
[170, 255]、弱通道[0, 169],两区间不重叠使assert.isAbove恒成立,而约 250 万种组合空间保证两次调用不同。 - 验收测试(Chai 断言)同时校验了输入域、输出形状(6 位、
[0-9A-F])、严格主导性和随机性四个维度,逐条对照测试写代码是解这类题最可靠的路径。 - 相关练习与实现证据:原题文件 6821ebf3237de8297eaee797.md、块结构 daily-coding-challenges-javascript.json、挑战类型定义 challenge-types.ts、同主题姊妹题 Challenge 23: RGB to Hex。
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考