跟着 MDN 学 JavaScript Day 30:JSON 技能实战——解析猫舍数据并动态展示

【多重好礼】OpenTiny NEXT 前端智能化系列直播征文活动 10w+人浏览 558人参与

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

在上一篇文章中,我们系统学习了 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 进行遍历操作,代码将无法正常工作,因为字符串不具备数组和对象的访问特性。

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

const catData = 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 (let i = 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 (let j = 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 代码的组织方式蕴含了一个关于异步编程的重要知识点。完整的脚本结构如下:

const section = document.querySelector('section');

let para1 = document.createElement('p');
let para2 = document.createElement('p');
let motherInfo = 'The mother cats are called ';
let kittenInfo;
const requestURL = 'https://mdn.github.io/learning-area/javascript/oojs/tasks/json/sample.json';

fetch(requestURL)
  .then(response => response.text())
  .then(text => displayCatInfo(text))

function displayCatInfo(catString) {
  let total = 0;
  let male = 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 函数的完整实现应当包含解析、循环、拼接三个核心部分。整合后的完整代码如下:

function displayCatInfo(catString) {
  let total = 0;
  let male = 0;

  const catData = JSON.parse(catString);

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

    for (let j = 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 基础将为你理解和操作对象数据结构提供有力的支撑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值