Profile Lookup 深度解析:对象数组查找、运行时属性访问与判空优先级
【免费下载链接】TVBoxOSCTVBoxOSC - 一个基于第三方项目的代码库,用于电视盒子的控制和管理。项目地址: https://gitcode.com/GitHub_Trending/tv/TVBoxOSC
freeCodeCamp 的 basic-javascript 块经典挑战 Profile Lookup,把对象数组查找与运行时属性访问两项核心功力压缩进十几行代码。读完你可以通过全部六条断言,并说清每一行为什么必须这样写。
挑战的课程坐标:为什么排在循环与递归之后
这道题在仓库中的坐标由三处配置共同确定。先看挑战文件 curriculum/challenges/english/blocks/basic-javascript/5688e62ea601b2482ff8422b.md 的 front-matter:
id: 5688e62ea601b2482ff8422btitle: Profile LookupchallengeType: 1forumTopicId: 18259dashedName: profile-lookup
challengeType: 1对应 packages/shared/src/config/challenge-types.ts 中的js = 1,即一道在浏览器内置控制台中运行的 JavaScript 函数补全题。curriculum/structure/blocks/basic-javascript.json 里,basic-javascript 块被声明为helpCategory: "JavaScript"、blockLayout: "legacy-challenge-list"、isUpcomingChange: false,属于现行课程的经典挑战列表。
再看challengeOrder:它紧随Replace Loops using Recursion之后,处在循环与递归章节的收尾位置。排在这里,默认解题就应当用循环遍历,而不是递归。仓库里还有一个同 ID 的 Lab 版 Build a Profile Lookup(challengeType: 26,对应jsLab),把它当作用户故事重构同一场景的加深路径即可,后文延伸部分会交代。
题面逐条翻译:对象数组查找的主键与三种输出形态
--description--原文提出六条要求,翻译成程序语义只有一句话:这是一个主键查找加属性校验问题,主键是firstName,输出只有三种形态——命中的属性值、No such contact、No such property。
题目给出的完整初始代码(--seed-contents--)如下,任何解法都必须在这份数据之上工作:
// Setup const contacts = [ { firstName: "Akira", lastName: "Laine", number: "0543236543", likes: ["Pizza", "Coding", "Brownie Points"], }, { firstName: "Harry", lastName: "Potter", number: "0994372684", likes: ["Hogwarts", "Magic", "Hagrid"], }, { firstName: "Sherlock", lastName: "Holmes", number: "0487345643", likes: ["Intriguing Cases", "Violin"], }, { firstName: "Kristian", lastName: "Vos", number: "unknown", likes: ["JavaScript", "Gaming", "Foxes"], }, ]; function lookUpProfile(name, prop) { // Only change code below this line // Only change code above this line } lookUpProfile("Akira", "likes");我们对 seed 数据做三个观察:
contacts是对象数组,每个元素的字段并不完全同构——number是字符串,likes是数组。prop以字符串形式传入,函数体内必须用括号语法obj[prop]取值。写成点语法读的是字面属性名prop,结果永远是undefined。likes的值是数组,断言要求原样返回,不做任何拼接。
六条断言逐条拆解:测试强制的行为契约
语义规则定了,断言把它量化成六条可执行的判定:
| 调用示例 | 期望结果 | 验证点 |
|---|---|---|
lookUpProfile("Kristian", "lastName") | "Vos" | 命中联系人后返回其字符串属性值 |
lookUpProfile("Sherlock", "likes") | ["Intriguing Cases", "Violin"] | 原样返回数组类型的属性值 |
lookUpProfile("Harry", "likes") | 数组(typeof为object) | 同样要求返回数组而非字符串 |
lookUpProfile("Bob", "number") | "No such contact" | name未命中时,prop再合法也先报联系人缺失 |
lookUpProfile("Bob", "potato") | "No such contact" | name与prop都无效时,优先报联系人缺失 |
lookUpProfile("Akira", "address") | "No such property" | 联系人存在但属性缺失时,报属性缺失 |
对应的原始断言代码:
assert(lookUpProfile('Kristian', 'lastName') === 'Vos'); assert.deepEqual(lookUpProfile('Sherlock', 'likes'), [ 'Intriguing Cases', 'Violin' ]); assert(typeof lookUpProfile('Harry', 'likes') === 'object'); assert(lookUpProfile('Bob', 'number') === 'No such contact'); assert(lookUpProfile('Bob', 'potato') === 'No such contact'); assert(lookUpProfile('Akira', 'address') === 'No such property');断言强制出两条行为契约:
- 判空优先级:只要
name匹配失败,立即返回No such contact,不再检查prop(第 4、5 行)。只有name命中后,才轮到判断prop是否有效(第 6 行)。 - 返回值类型可变:命中数组就原样返回数组,不要用
.join()转成字符串(第 2、3 行)。
参考解法逐行过:为什么必须用括号与 || 兜底
判定优先级定了,看--solutions--中的参考解法如何兑现它:
// ...(同 seed,省略) function lookUpProfile(name, prop) { for (let i in contacts) { if (contacts[i].firstName === name) { return contacts[i][prop] || "No such property"; } } return "No such contact"; }三段逻辑分工明确:
- 外层
for...in遍历下标0到3。循环走完仍未命中,说明通讯录里不存在该firstName,落到函数末尾返回No such contact——恰好覆盖断言 4、5。 - 内层命中分支:一旦
contacts[i].firstName === name,立刻用括号语法读contacts[i][prop]。prop是运行时变量,只有括号写法能把它解释成"名为prop的属性"。 ||短路兜底:读到的值是undefined(属性不存在)时,逻辑或把结果替换成"No such property",满足断言 6。
但||有一个边界值得警惕:属性确实存在、值恰好是""、0、false、null时,合法值会被误判成No such property。本题数据里number、lastName、likes全是真值,所以测试全过;下一节给出不依赖真值判断的写法,把这个隐患连根拔除。
更严谨的替代实现:for...of + hasOwnProperty
// ...(同 seed,省略) function lookUpProfile(name, prop) { for (const contact of contacts) { if (contact.firstName === name) { if (Object.prototype.hasOwnProperty.call(contact, prop)) { return contact[prop]; } return "No such property"; } } return "No such contact"; }两处写法的理由:
for...of直接遍历对象本体,省去下标中转,语义更直观。Object.prototype.hasOwnProperty.call(contact, prop)比prop in contact严谨在两点。in运算符沿原型链查找,prop撞上Object.prototype的内置成员(如constructor、toString)时,会误判属性存在,且返回继承值。.call形式固定了接收者,即使contact重写过自己的hasOwnProperty,调用的仍是原始实现。
两种方案对照:
| 判定维度 | 官方解法(for...in+\|\|) | 推荐解法(for...of+hasOwnProperty) |
|---|---|---|
| 遍历方式 | 遍历下标i,再contacts[i]取值 | 直接遍历对象元素 |
| 属性存在性判断 | 依赖undefined \|\| 默认值短路 | 显式调用hasOwnProperty |
| 假值处理 | 假值被误判为No such property | 正确返回假值本身 |
| 断言通过情况 | 全部通过 | 全部通过 |
无论哪种写法,守住三个不变量:判定顺序固定(先找name,命中后再判prop);取值必须用括号语法;返回值类型可变,数组原样返回。
🧨 高频踩坑点:四个错误写法写进记忆
点语法恒返回 undefined
- 错误:
return contact.prop; - 正确:
return contact[prop]; - 为什么错:点语法读字面键
"prop",而不是参数指向的属性名,函数恒返回undefined,六条断言全灭。
判定顺序颠倒
- 错误:先校验
prop,再查name - 正确:先全量匹配
name,在命中的对象上判prop - 为什么错:
lookUpProfile("Bob", "potato")必须报No such contact,顺序一翻就返回No such property,与断言 5 直接冲突。
跨对象校验属性
- 错误:遍历前对整个数组或首个对象统一校验
prop - 正确:在"实际匹配到的那个联系人对象"上判断存在性
- 为什么错:上表中的
contacts字段不同构,prop对某个人合法,对另一个人未必合法。
数组值被字符串化
- 错误:
return contact[prop].join(","); - 正确:
return contact[prop]; - 为什么错:断言 2、3 用
deepEqual与typeof校验数组本体,任何拼接或取首元素都会破坏契约。
文件消费链路:从 Joi 校验到 assert 执行
会解只是一半,这个 Markdown 文件如何被消费是另一半。
- Schema 校验:front-matter 字段全部受 curriculum/schema/challenge-schema.js 中 Joi 模式约束——
challengeType限定为Joi.number().min(0).max(33).required(),dashedName必须匹配小写 slug 正则^[a-z0-9-]+$,forumTopicId为可选数字。本题的challengeType: 1、dashedName: profile-lookup、forumTopicId: 18259全部命中约束。 - 构建解析:
--description--、--hints--、--seed-contents--、--solutions--等分段标记被构建工具解析为结构化数据——description渲染为题面,seed提供可编辑初始代码,solutions作为参考答案集。 - 编辑器注入:浏览器端编辑器把
contacts数组与函数骨架加载进只读区域,只放开// Only change code below this line与// Only change code above this line两行之间的区域供编辑。 - 断言执行:测试脚本对补全后的函数逐条运行
assert,第三节的六条断言就是这里的执行场景。
🧪 延伸实践:四个练习吃透这道题
- 把官方解法改写成
for...of版本,对照第五节的表格逐行比对行为,确认六条断言依旧全过。 - 给某联系人补一个
age: 0的手工用例,观察||版本误判假值,hasOwnProperty版本正确返回0。 - 在控制台跑
console.log(lookUpProfile("Kristian", "likes"))与console.log(lookUpProfile("Kristian", "lastName")),确认数组与字符串两类返回值。 - 通读 curriculum/schema/challenge-schema.js 中
challengeType、dashedName、forumTopicId的校验规则,理解课程内容为何能以纯 Markdown 加结构化约束长期演进。
经典版刷完,可以转向同场景的 Lab 版 Build a Profile Lookup(challengeType: 26,即jsLab),它以用户故事加自由建站的方式,要求你把本挑战的数组查找逻辑组织进真实小应用。
Profile Lookup 把对象数组遍历、运行时属性访问、函数返回值与判空优先级四门功课压进十几行代码。这道题吃透,课程里同构的查找类挑战就只是换一批数据。
【免费下载链接】TVBoxOSCTVBoxOSC - 一个基于第三方项目的代码库,用于电视盒子的控制和管理。项目地址: https://gitcode.com/GitHub_Trending/tv/TVBoxOSC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考