跟着 MDN 学 JavaScript Day 30:JSON 技能实战——解析猫舍数据并动态展示
2026/6/14 18:27:49 网站建设 项目流程

引言:从理论到实践的跨越

在上一篇文章中,我们系统学习了 JSON 的基础知识,包括它的语法结构、数据访问方式以及序列化与反序列化的核心方法。理论知识固然重要,但真正的掌握来自于动手实践。MDN 为我们准备了一道经典的 JSON 技能测试题,要求我们解析一组关于母猫及其小猫的 JSON 数据,并将统计结果动态展示在网页上。这道题目看似简单,却巧妙地串联起了 JSON 解析、循环嵌套、字符串拼接、异步编程等多个关键知识点,是检验 JSON 理解程度的绝佳试金石。

本文将详细拆解这道技能测试题的解题思路与实现过程。我们将从分析 JSON 数据结构入手,逐步讲解如何解析文本为对象、如何设计循环逻辑遍历嵌套数组、如何优雅地处理字符串拼接的边界情况,以及为何 DOM 更新操作必须放在异步回调函数内部。通过完整的代码实现和深入的原理解析,你将获得处理真实 JSON 数据的扎实能力。

题目解析:任务目标与数据结构

题目为我们提供了一组描述母猫及其小猫的 JSON 数据,存储在一个名为sample.json的文件中。数据通过 Fetch API 以文本形式加载到页面,并作为catString参数传递给displayCatInfo函数。我们需要在该函数内部完成两项统计任务:第一项是将所有母猫的名字用逗号分隔并拼接成一个完整的句子,存储在motherInfo变量中;第二项是统计所有小猫的总数以及雄性和雌性的数量,同样拼接成句子存储在kittenInfo变量中。两个变量的值最终会通过para1para2这两个段落元素展示在页面上。

理解 JSON 数据的结构是编写正确代码的前提。sample.json的内容是一个数组,其中每个元素都是一个代表母猫的对象。每个母猫对象包含四个属性:name表示名字,breed表示品种,color表示毛色,而kittens则是一个数组,包含该母猫的所有小猫信息。每只小猫又是一个对象,拥有namegender两个属性,gender的值为"m"表示雄性,"f"表示雌性。

[ { "name" : "Lindy", "breed" : "Cymric", "color" : "white", "kittens" : [ { "name" : "Percy", "gender" : "m" }, { "name" : "Thea", "gender" : "f" }, { "name" : "Annis", "gender" : "f" } ] }, { "name" : "Mina", "breed" : "Aphrodite Giant", "color" : "ginger", "kittens" : [ { "name" : "Doris", "gender" : "f" }, { "name" : "Pickle", "gender" : "f" }, { "name" : "Max", "gender" : "m" } ] }, { "name" : "Antonia", "breed" : "Ocicat", "color" : "leopard spotted", "kittens" : [ { "name" : "Bridget", "gender" : "f" }, { "name" : "Randolph", "gender" : "m" } ] } ]

观察数据结构可以发现,这是一个典型的两层嵌套关系:外层数组遍历母猫,内层数组遍历每只母猫的小猫。这种结构自然地引导我们采用外部循环加内部循环的双层遍历策略。外部循环负责收集母猫的名字并拼接到motherInfo字符串中,内部循环则遍历当前母猫的kittens数组,累加小猫总数并根据gender值分别统计雄性和雌性的数量。

第一步:将 JSON 文本解析为 JavaScript 对象

题目明确指出,JSON 数据在displayCatInfo函数内以文本形式提供,这意味着catString参数是一个原始的 JSON 字符串,不能直接使用点表示法或括号表示法来访问其中的属性。在操作数据之前,必须先将这个字符串解析为 JavaScript 对象。这正是JSON.parse方法的用武之地。

displayCatInfo函数内部,第一行代码应该调用JSON.parse并传入catString,将返回值赋给一个变量,比如catData。这一步骤至关重要,因为只有经过解析,JSON 才能从无结构的字符串转变为可按索引和属性名访问的对象数组。如果忘记调用JSON.parse而直接对catString进行遍历操作,代码将无法正常工作,因为字符串不具备数组和对象的访问特性。

解析操作的核心代码非常简洁:

constcatData=JSON.parse(catString);

这行代码执行后,catData就成为了与原始 JSON 结构完全对应的 JavaScript 数组。此时我们可以安全地使用catData.length获取母猫的数量,使用catData[0].name获取第一只母猫的名字,使用catData[1].kittens[2].gender获取第二只母猫第三只小猫的性别。所有在上一篇文章中学到的链式访问技巧在这里都能派上用场。JSON.parse是反序列化的标准方法,它与JSON.stringify互为逆操作,两者共同构成了 JavaScript 中数据序列化与反序列化的完整工具链。

第二步:外部循环遍历母猫并拼接名字

数据解析完成后,首要任务是构建motherInfo字符串。题目已经为motherInfo变量提供了初始值"The mother cats are called ",我们只需在此基础上追加所有母猫的名字,并在最后一只猫的名字前添加"and"字样,在句末添加句号。

处理这个问题有多种策略。一种直观的思路是先用循环将所有母猫名字收集到一个数组中,然后使用数组的join方法以逗号加空格作为分隔符进行拼接,最后手动处理最后一个逗号和"and"的替换逻辑。另一种更灵活的方式是在循环中直接判断当前索引位置,对最后一只猫进行特殊处理。考虑到题目要求无论 JSON 中有多少只猫都能正常工作,我们应当避免硬编码任何具体的名字或数量,而是依赖数组的长度和索引动态判断。

采用循环内判断索引的方式,核心逻辑可以这样实现:

for(leti=0;i<catData.length;i++){if(i===catData.length-1){motherInfo+=`and${catData[i].name}.`;}else{motherInfo+=`${catData[i].name},`;}}

在这段代码中,循环变量i0递增到数组长度减一。对于每一次迭代,我们检查当前索引是否等于catData.length - 1,即是否到达了最后一只母猫。如果条件成立,说明这是最后一只,在名字前拼接"and ",名字后拼接句号。如果不成立,则在名字后拼接逗号和空格作为分隔符。这种基于索引的条件判断保证了代码对任意数量母猫的适用性。无论将来 JSON 数据中添加或删除母猫,代码都能自动调整输出格式。最终motherInfo的值将类似于"The mother cats are called Lindy, Mina and Antonia."

第三步:内部循环统计小猫数量与性别

在外部循环处理每只母猫的同时,我们需要开启第二个循环来遍历当前母猫的kittens数组,完成小猫的统计工作。题目要求统计三个数值:所有小猫的总数、雄性小猫的数量以及雌性小猫的数量。total变量用于累加总数,每次内部循环迭代就自增一。male变量用于累计雄性数量,需要判断每只小猫的gender属性是否等于字符串"m"。雌性数量则可以通过total减去male间接得出,无需单独维护计数器。

内部循环的实现代码如下:

for(letj=0;j<catData[i].kittens.length;j++){total++;if(catData[i].kittens[j].gender==="m"){male++;}}

内部循环直接嵌套在外部循环之中,这意味着对于每一只母猫,都会完整遍历一次它的小猫列表。循环中首先无条件执行total自增,确保每只小猫都被计入总数。然后检查当前小猫的gender属性,只有当它严格等于字符串"m"时,才让male变量自增。这种分别处理的方式让统计逻辑清晰明了,total追踪的是小猫总量,male追踪的是其中的雄性个体,而雌性数量则由两者之差自动体现。

完成所有循环后,我们需要使用这些统计数据构建kittenInfo字符串。由于题目没有为kittenInfo提供初始值,需要从零开始构造完整句子:

kittenInfo=`There are${total}kittens,${male}are male and${total-male}are female.`;

这行代码使用了模板字面量语法,将totalmale以及total - male的计算结果直接嵌入到句子结构中。模板字面量使用反引号包围,以${}包裹表达式,相比传统的字符串拼接方式更加直观易读。最终kittenInfo的值将类似于"There are 8 kittens, 3 are male and 5 are female."。使用total - male来计算雌性数量,避免了维护第三个计数器的需要,保持了代码的简洁性。

第四步:理解异步代码中的 DOM 更新时机

题目提供的 HTML 骨架中,JavaScript 代码的组织方式蕴含了一个关于异步编程的重要知识点。完整的脚本结构如下:

constsection=document.querySelector('section');letpara1=document.createElement('p');letpara2=document.createElement('p');letmotherInfo='The mother cats are called ';letkittenInfo;constrequestURL='https://mdn.github.io/learning-area/javascript/oojs/tasks/json/sample.json';fetch(requestURL).then(response=>response.text()).then(text=>displayCatInfo(text))functiondisplayCatInfo(catString){lettotal=0;letmale=0;// 解析和统计代码在此处para1.textContent=motherInfo;para2.textContent=kittenInfo;}section.appendChild(para1);section.appendChild(para2);

观察这段代码的执行顺序,可以清楚地看到异步编程的影响。脚本首先执行同步代码:获取section元素、创建两个段落元素、初始化motherInfo、定义requestURL。然后调用fetch函数发起网络请求。由于fetch是异步的,JavaScript 引擎不会等待请求完成,而是继续执行后面的同步代码,也就是将para1para2追加到section中。至此,页面上已经有了两个段落元素,但它们的textContent尚未被设置为最终的值。

当网络请求完成后,then回调链中的函数依次执行:response.text将响应体提取为文本,然后displayCatInfo被调用。在displayCatInfo内部,JSON 文本被解析,双层循环完成统计,motherInfo被拼接完整,kittenInfo被构造完成,最后两行赋值语句将这两个字符串分别设置给para1.textContentpara2.textContent。此时,页面上的两个段落元素才真正显示出有意义的内容。

题目特意提出了一个引导性思考:为什么para1.textContentpara2.textContent的赋值语句放在displayCatInfo函数内部,而不是放在脚本末尾?如果将这些赋值语句移到脚本末尾,它们会在fetch请求尚未完成、displayCatInfo尚未执行时就被调用。此时motherInfo还只是"The mother cats are called "这个初始前缀,kittenInfo甚至是undefined,页面上显示的将是残缺的内容。将赋值语句放在displayCatInfo函数内部,确保了这些操作只有在 JSON 数据成功获取并解析之后才会执行,从而保证页面展示的是完整正确的结果。

完整代码实现与逻辑串联

将以上所有步骤整合起来,displayCatInfo函数的完整实现应当包含解析、循环、拼接三个核心部分。整合后的完整代码如下:

functiondisplayCatInfo(catString){lettotal=0;letmale=0;constcatData=JSON.parse(catString);for(leti=0;i<catData.length;i++){if(i===catData.length-1){motherInfo+=`and${catData[i].name}.`;}else{motherInfo+=`${catData[i].name},`;}for(letj=0;j<catData[i].kittens.length;j++){total++;if(catData[i].kittens[j].gender==="m"){male++;}}}kittenInfo=`There are${total}kittens,${male}are male and${total-male}are female.`;para1.textContent=motherInfo;para2.textContent=kittenInfo;}

这段代码的执行流程清晰而连贯。首先声明totalmale计数器并初始化为零。然后调用JSON.parsecatString解析为catData数组。接着进入外部for循环,遍历每一只母猫。在外部循环体内,先通过if条件判断当前母猫是否为最后一只,按规则将名字追加到motherInfo字符串中。然后立即进入内部for循环,遍历当前母猫的kittens数组,每次迭代total自增一,遇到gender"m"的小猫则male自增一。所有循环结束后,使用模板字面量构造kittenInfo字符串。最后将两个字符串分别赋值给两个段落元素的textContent属性。整个过程一气呵成,每一行代码都有明确的职责,构成了一个完整的数据处理管道。

边界情况与扩展思考

这道题目虽然以三只母猫的固定数据为背景,但代码的设计应当具备对动态数据的适应能力。题目提示特别强调"如何确保无论 JSON 中有多少只猫都能正常工作",这正是考察开发者编写健壮代码的意识。我们的实现中使用了数组长度判断、索引比较等通用方法,而非硬编码特定名字或数量,因此能够天然适应数据规模的变化。

如果将来 JSON 中只有一只母猫,循环只执行一次。此时i等于0,同时catData.length - 1也等于0if条件成立,代码执行motherInfo += "and " + catData[0].name + "."这一分支。最终输出的字符串将是"The mother cats are called and Lindy."。虽然语法上没有错误,但从语义上看,只有一只猫时使用"and"显得有些奇怪。在真正的生产环境中,可以对此进行优化,例如当数组长度为一时完全不使用逗号或"and",直接将名字拼接上去。这提醒我们,通用逻辑虽然覆盖了主要场景,但边界情况往往需要额外考量。

另一个值得思考的点是代码中计数器变量的声明位置。totalmale被声明在displayCatInfo函数的作用域内,而非全局作用域。这是一个好的实践,因为它限定了变量的生命期和可见范围,避免了全局命名空间的污染。如果将来这个函数被多次调用,每次调用都会创建全新的totalmale变量,不会相互干扰。这种基于函数作用域的封装思想,是编写可维护 JavaScript 代码的基础习惯。

异步处理模式的选择同样值得讨论。题目中使用了传统的then回调链来处理fetch的结果,这是 Promise 的经典用法。如今asyncawait语法提供了更接近同步代码的书写体验。如果使用async函数重写,可以将整个fetch逻辑包裹在一个async函数中,使用await等待请求结果,使得代码流程更加直观。不过理解then链的运作机制依然是掌握异步编程的必经之路,因为它是 Promise 对象最基础的使用方式,也是许多老旧代码库中仍在广泛使用的模式。

总结:JSON 技能的实战验收

本文围绕 MDN 的 JSON 技能测试题,完整呈现了从数据解析到页面渲染的全过程。我们首先分析了sample.json的数组嵌套结构,明确了外部循环处理母猫、内部循环统计小猫的策略。接着详细讲解了JSON.parse将文本转换为可操作对象的关键作用。然后分别处理了motherInfo字符串的动态拼接逻辑,以及kittenInfo字符串中总数与性别统计的累加方法。最后深入探讨了异步代码中 DOM 更新时机的重要性,解释了为何textContent赋值必须放置在回调函数内部。

这道题目虽然篇幅不大,但浓缩了 JSON 操作的核心技能:解析、遍历、访问嵌套属性、统计计算以及结合 DOM 进行结果展示。掌握这些技能意味着你具备了处理 Web 应用中绝大多数 JSON 数据场景的能力。无论是解析服务器返回的列表数据、处理复杂嵌套的配置信息,还是构造用于提交到后端的 JSON 请求体,其底层逻辑都与本文所演示的技术一脉相承。

JSON 的学习至此告一段落。在掌握了数据结构传输的基础之后,下一阶段我们将转向 JavaScript 中的面向对象编程,探索如何更好地组织和抽象代码逻辑,构建更加模块化和可维护的应用程序。扎实的 JSON 基础将为你理解和操作对象数据结构提供有力的支撑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询