引言:数据交换的隐形桥梁

在现代 Web 开发中,数据无处不在。无论是浏览社交媒体的动态流、查看天气预报的实时更新,还是在线购物时的商品列表,所有这些信息都需要在服务器与客户端之间进行高效、可靠的传输。JavaScript 对象表示法(JSON,全称 JavaScript Object Notation)正是承担这一关键任务的标准格式。它由道格拉斯·克罗克福特推广,虽然语法源自 JavaScript,但如今已完全独立于任何编程语言,成为众多程序环境共同认可的数据交换格式。

本文将深入探讨 JSON 的方方面面。我们将从 JSON 的基本概念和语法结构讲起,通过一个实际案例展示如何从远程服务器获取 JSON 数据并动态渲染到网页上,接着详细解释 JavaScript 对象与 JSON 字符串之间相互转换的两种核心方法,最终为你构建起对 JSON 这一重要工具的完整认知。掌握 JSON,你就掌握了 Web 数据传输中最基础也最实用的一环。

什么是 JSON:一种纯数据格式

JSON 是一种严格按照 JavaScript 对象语法组织的纯数据格式。它最核心的特征在于"纯数据"——这意味着 JSON 只包含属性及其对应的值,绝不可能包含方法或函数。这一约束使得 JSON 轻量、干净,适合在网络上快速传输。当我们从服务器获取数据或向服务器提交数据时,通常处理的不是可以直接操作的 JavaScript 对象,而是一段结构清晰的文本字符串。

JSON 在实际应用中有两种存在形态:其一是在程序中作为对象存在,此时数据可以被 JavaScript 代码直接访问和修改;其二是作为字符串存在,这种形态专门用于网络传输。字符串形态的 JSON 可以轻松跨越不同的系统和平台,被任何支持 JSON 解析的环境读取和生成。JavaScript 提供了一个全局的 JSON 对象,专门负责在这两种形态之间进行转换。将字符串形式的 JSON 转换为原生 JavaScript 对象的过程,专业术语称为反序列化;而将原生对象转换为可传输的字符串,则称为序列化。理解这两种形态的区别及其转换过程,是使用 JSON 的基础。

一个完整的 JSON 数据通常存储在以 .json 为扩展名的文本文件中,其 MIME 类型为 application/json。当你从服务器请求一个 JSON 文件时,你得到的本质就是一个纯文本内容,只不过它的格式严格遵循 JSON 语法规范。这种规范定义了什么形式的文本是有效的 JSON,什么形式的文本会导致解析错误。

JSON 的结构:对象与数组的层次化组合

JSON 的语法结构极其类似于 JavaScript 对象字面量的写法。你可以在 JSON 中使用字符串、数字、数组、布尔值、null 以及其他嵌套的对象字面量。这种灵活性使得你可以构建出任意深度的数据层次,清晰表达现实世界中复杂的数据关系。

{
  "squadName": "Super hero squad",
  "homeTown": "Metro City",
  "formed": 2016,
  "secretBase": "Super tower",
  "active": true,
  "members": [
    {
      "name": "Molecule Man",
      "age": 29,
      "secretIdentity": "Dan Jukes",
      "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
    },
    {
      "name": "Madame Uppercut",
      "age": 39,
      "secretIdentity": "Jane Wilson",
      "powers": [
        "Million tonne punch",
        "Damage resistance",
        "Superhuman reflexes"
      ]
    },
    {
      "name": "Eternal Flame",
      "age": 1000000,
      "secretIdentity": "Unknown",
      "powers": [
        "Immortality",
        "Heat Immunity",
        "Inferno",
        "Teleportation",
        "Interdimensional travel"
      ]
    }
  ]
}

以上这段 JSON 示例展示了典型的数据层次结构。最外层是一个对象,包含了队伍名称、家乡、成立年份、秘密基地、活跃状态以及成员列表等属性。squadNamehomeTown 是字符串类型,formed 是数字类型,active 是布尔值,而 members 则是一个数组,数组中的每个元素又是一个对象,描述了每位英雄的详细信息,包括姓名、年龄、秘密身份以及超能力列表。这种对象与数组的嵌套组合,构成了 JSON 表达结构化数据的核心能力。

当这段 JSON 字符串被加载到 JavaScript 程序中并解析后,假设我们将其赋值给变量 superHeroes,那么访问其中的数据就如同操作普通的 JavaScript 对象一样。点表示法和括号表示法均适用:

superHeroes.hometown;
superHeroes["active"];

这两行代码分别访问了家乡属性和活跃状态属性。当需要访问更深层次的数据时,则需要将属性名和数组索引按顺序链接起来。例如,要获取 members 数组中第二个英雄的第三个超能力,表达式如下:

superHeroes["members"][1]["powers"][2];

这条链式访问可以这样拆解:首先通过变量名 superHeroes 指向存储的对象,然后通过 ["members"] 访问 members 属性,由于 members 是一个包含对象的数组,使用 [1] 索引获取第二个元素,接着在该对象内部通过 ["powers"] 访问 powers 属性,而 powers 本身又是一个数组,最终用 [2] 索引取得第三个超能力字符串。

这种链式访问的思维方式对于处理嵌套 JSON 数据至关重要。你需要清晰地理解每一层数据的类型——是对象还是数组——从而决定使用属性名还是数字索引来继续深入。多做几次练习,这种数据导航能力很快就会成为你的本能。

JSON 数组:另一种合法的顶层结构

JSON 并非只能以对象作为顶层结构。完全由数组构成的 JSON 同样是合法且常见的。下面这段 JSON 去掉了外层对象,直接将英雄数组作为顶层元素:

[
  {
    "name": "Molecule Man",
    "age": 29,
    "secretIdentity": "Dan Jukes",
    "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
  },
  {
    "name": "Madame Uppercut",
    "age": 39,
    "secretIdentity": "Jane Wilson",
    "powers": [
      "Million tonne punch",
      "Damage resistance",
      "Superhuman reflexes"
    ]
  }
]

这段 JSON 以方括号开头,表示整个数据是一个数组。访问其中的元素只需要通过数组索引,例如 [0]["powers"][0] 即可获取第一个英雄的第一个超能力。JSON 数组结构的灵活性在于,当数据本身就是一个集合而没有额外需要描述的元数据时,使用数组作为顶层结构最为直接。在实际开发中,许多 API 返回的列表数据正是采用这种形式。

在编写 JSON 时,有若干细节必须严格遵守。JSON 对语法的要求比 JavaScript 对象字面量更为严格。所有属性名必须使用双引号包围,单引号在此无效。字符串值同样必须使用双引号。逗号和方括号的错位、多余的逗号、缺失的引号,任何一个微小的错误都会导致整个 JSON 解析失败。因此,在处理手工编写的 JSON 数据时,使用 JSONLint 等在线验证工具进行检查是明智的做法。此外,JSON 的数据类型范围是有限的,它可以表示字符串、数字、布尔值、null、对象和数组,但不支持 undefined、函数、日期对象等 JavaScript 特有的类型。数值不能是 NaNInfinity,这些都必须注意。

动手实践:使用 Fetch API 获取并渲染 JSON 数据

理论知识需要通过实践来巩固。我们将通过一个完整的示例来展示如何在实际网页中获取远程 JSON 数据,并利用 DOM 操作将其内容动态展示出来。这个示例的核心文件包括一个 HTML 页面和一个 CSS 样式表。HTML 页面中已经预留了 <header><section> 以及 <script> 标签的位置,我们的 JavaScript 代码将负责填充这些区域。示例所用到的 JSON 数据托管在 GitHub 上,地址为 https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json

整个流程由一个名为 populate 的顶层异步函数驱动。该函数的实现如下:

async function populate() {
  const requestURL =
    "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json";
  const request = new Request(requestURL);

  const response = await fetch(request);
  const superHeroes = await response.json();

  populateHeader(superHeroes);
  populateHeroes(superHeroes);
}

这段代码的核心在于使用了 Fetch API,这是一个允许 JavaScript 发起网络请求的现代接口。与传统的 XMLHttpRequest 不同,Fetch API 基于 Promise 设计,语法更为简洁。在函数中,我们首先声明了存储 JSON 文件 URL 的变量 requestURL,接着用这个 URL 创建一个 Request 对象。随后调用 fetch 函数发起请求,它返回一个 Response 对象。fetch 本质上是异步操作,因此我们使用 await 关键字等待其完成。得到的 response 对象上有一个 json 方法,该方法会读取响应体并将其解析为 JavaScript 对象,这同样是一个异步过程,因此再次使用 await

值得注意的是,使用 Fetch API 的函数必须声明为 async,这正是 populate 函数前 async 关键字的来由。关于异步编程的深入原理,我们将在后续的学习中详细探讨。在此,只需记住 fetch 返回的是 Promise,而 await 用于等待 Promise 的解决结果。

获取到 JavaScript 对象 superHeroes 之后,我们将其传递给两个函数:populateHeader 负责填充页眉,populateHeroes 负责为每位英雄创建信息卡片。这样的职责拆分使得代码逻辑清晰、便于维护。

动态创建页面内容:从 JSON 到 DOM

populateHeader 函数负责将 JSON 中的队伍信息渲染到页面的 <header> 区域。其实现代码如下:

function populateHeader(obj) {
  const header = document.querySelector("header");
  const myH1 = document.createElement("h1");
  myH1.textContent = obj.squadName;
  header.appendChild(myH1);

  const myPara = document.createElement("p");
  myPara.textContent = `Hometown: ${obj.homeTown} // Formed: ${obj.formed}`;
  header.appendChild(myPara);
}

该函数接收一个参数 obj,即我们解析得到的 JavaScript 对象。首先通过 querySelector 选择页面中的 <header> 元素,然后使用 createElement 创建一个 <h1> 元素,将其 textContent 设置为 objsquadName 属性值,最后用 appendChild 将这个标题添加到页眉中。紧接着,以类似的操作创建了一个段落元素,其文本内容使用了模板字面量,将 objhomeTownformed 属性值动态嵌入其中。模板字面量是 ES6 引入的字符串语法,使用反引号包围,用 ${} 语法嵌入表达式,相比传统的字符串拼接更加优雅和易读。

接下来是 populateHeroes 函数,它负责创建每一位英雄的信息卡片。这是整个示例中代码最丰富的部分:

function populateHeroes(obj) {
  const section = document.querySelector("section");
  const heroes = obj.members;

  for (const hero of heroes) {
    const myArticle = document.createElement("article");
    const myH2 = document.createElement("h2");
    const myPara1 = document.createElement("p");
    const myPara2 = document.createElement("p");
    const myPara3 = document.createElement("p");
    const myList = document.createElement("ul");

    myH2.textContent = hero.name;
    myPara1.textContent = `Secret identity: ${hero.secretIdentity}`;
    myPara2.textContent = `Age: ${hero.age}`;
    myPara3.textContent = "Superpowers:";

    const superPowers = hero.powers;
    for (const power of superPowers) {
      const listItem = document.createElement("li");
      listItem.textContent = power;
      myList.appendChild(listItem);
    }

    myArticle.appendChild(myH2);
    myArticle.appendChild(myPara1);
    myArticle.appendChild(myPara2);
    myArticle.appendChild(myPara3);
    myArticle.appendChild(myList);

    section.appendChild(myArticle);
  }
}

函数首先获取页面中的 <section> 元素,并将 obj.members 赋值给局部变量 heroes。接着使用 for...of 循环遍历 heroes 数组中的每一位英雄。对于每位英雄,函数创建了一个 <article> 元素作为信息卡的容器,以及内部所需的 <h2>、三个 <p> 和一个 <ul> 元素。<h2> 被设置为英雄的名字,三个段落分别展示秘密身份、年龄和 “Superpowers:” 标签。英雄的超能力列表存储在 powers 属性中,将其赋值给 superPowers 变量后,通过内层循环为每一个超能力创建一个 <li> 元素,填充文本后追加到 <ul> 中。

所有子元素创建完成后,按照语义顺序将 <h2>、三个 <p><ul> 依次追加到 <article> 中,最后将完整的 <article> 追加到 <section> 容器内。追加的顺序直接影响 HTML 中的展示顺序,因此需要根据期望的排版结构仔细安排。调用 appendChild 的顺序,就是元素在父元素内部的排列顺序。

整个过程的关键在于理解 JSON 数据结构和 DOM 操作之间的映射关系。每一级 JSON 嵌套对应着新元素的创建与插入,循环用于处理数组类型的数据。这种模式是前端渲染列表数据的基础,在各类前端框架出现之前,原生 JavaScript 的这类操作构成了单页应用动态内容更新的核心手段。最后,在脚本底部调用 populate 函数即可启动整个流程。

对象与文本的转换:JSON.parseJSON.stringify

在前面的示例中,我们直接使用 response.json 方法一步到位地将网络响应解析为 JavaScript 对象,这得益于 Fetch API 的便捷封装。但在实际开发中,情况并非总是如此理想。有时我们接收到的数据是原始的 JSON 文本字符串,需要手动将其转换为对象;有时我们需要将本地的 JavaScript 对象转换为 JSON 字符串以通过 AJAX 请求发送给服务器。浏览器为此内建了 JSON 对象,其上提供了两个核心方法:parsestringify

JSON.parse 方法接受一个 JSON 格式的文本字符串作为参数,解析后返回对应的 JavaScript 对象。在获取数据时,如果使用了 response.text 而不是 response.json,我们就需要手动调用 parse。下面的代码片段展示了这一过程:

async function populate() {
  const requestURL =
    "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json";
  const request = new Request(requestURL);

  const response = await fetch(request);
  const superHeroesText = await response.text();

  const superHeroes = JSON.parse(superHeroesText);
  populateHeader(superHeroes);
  populateHeroes(superHeroes);
}

这里先用 text 方法将响应体读取为纯文本,然后通过 JSON.parse 将该文本解析为可操作的 JavaScript 对象。这种两步走的方式虽然稍显冗长,但赋予了开发者更多控制权,例如可以在解析前对文本进行校验或预处理。

parse 相反的操作是 JSON.stringify。它接收一个 JavaScript 对象作为参数,返回该对象对应的 JSON 字符串。这个方法在需要将客户端数据发送至服务器时尤为实用。你可以在浏览器控制台中进行以下实验:

let myObj = { name: "Chris", age: 38 };
myObj;
let myString = JSON.stringify(myObj);
myString;

首先创建了一个包含 nameage 属性的对象,直接输入 myObj 会显示对象本身。然后调用 JSON.stringify 将其序列化,结果 myString 变成了一个字符串 '{"name":"Chris","age":38}'。仔细观察,属性名被自动加上了双引号,符合 JSON 的严格规范。如果对象中包含 JSON 不支持的数据类型,例如函数或 undefined,这些值在序列化过程中会被忽略或转换为 nullstringify 还可以接受额外的参数来控制缩进格式和序列化的过滤规则,这些进阶用法在你需要生成可读性良好的 JSON 文件或选择性导出属性时非常有用。

JSON 使用中的常见注意事项

在实际工作中使用 JSON 时,有几个容易被忽视但至关重要的细节需要牢记。首先,JSON 严格区分大小写,属性名的大小写不一致会导致无法正确访问数据。其次,JSON 字符串中的数值不能是 NaNInfinity,这些值在 JavaScript 中合法但在 JSON 中非法,尝试序列化包含这些值的对象会抛出错误。再次,JSON 对尾随逗号零容忍,对象或数组的最后一个元素后面不能有多余的逗号,而 JavaScript 对象字面量在某些环境中允许尾随逗号存在。最后,在从不可信来源接收 JSON 数据时,应当始终使用 try...catch 包裹 JSON.parse 调用,以防因格式错误导致整个程序崩溃。

JSON 设计上的纯粹性成就了它的通用性。正是因为它剔除了方法、仅保留数据、统一了字符串引号规则,才使得各种编程语言都能毫无歧义地解析和生成 JSON。当你在浏览器控制台中观察 JSON 字符串时,你能看到的只有数据本身,没有行为,没有环境依赖。这种极简主义的设计哲学,正是 JSON 超越 JavaScript 边界、成为 Web 数据交换基石的根本原因。

技能检验:巩固你的 JSON 知识

在这里插入图片描述

学习任何技术的最佳验证方式都是亲自动手实践。在结束本文的学习之前,建议你完成 MDN 提供的技能测试:技能测试——JSON。测试中涵盖了 JSON 的基本语法辨析、数据访问路径的书写以及序列化与反序列化的基本用法。通过这些练习,你能够检测自己是否真正掌握了 JSON 的核心要点。

此外,你还可以尝试一些扩展练习。例如,编写你自己的 JSON 数据来描述一个图书馆的藏书信息,包含书名、作者、出版年份、分类标签等字段,然后写一个 JavaScript 程序来解析并展示这些数据。或者尝试使用 JSON.stringify 的第二个和第三个参数来控制输出格式,观察它们对生成字符串的影响。实践出真知,越是动手调试,你对 JSON 的理解就越深入透彻。

总结:承前启后的数据枢纽

本文系统地介绍了 JSON 这一贯穿现代 Web 开发的基础数据格式。我们从 JSON 的两种存在形态讲起,辨析了传输形态的字符串与程序形态的对象之间的区别与联系。接着详细拆解了 JSON 的语法结构,讲解了对象和数组如何嵌套构成任意深度的数据层次,以及如何使用链式访问路径导航到目标数据。随后通过一个完整的 Fetch API 示例,演示了从获取远程 JSON 数据到动态创建 DOM 元素的完整流程。最后深入分析了 JSON.parseJSON.stringify 这两个核心转换方法的作用与使用场景。

JSON 的知识点是承前启后的枢纽。它连接了数据在服务器端的存储与在客户端的呈现,也连接了网络请求与 DOM 操作。掌握了 JSON,你就具备了处理 Web 数据的基本能力,为接下来深入学习面向对象的 JavaScript 编程以及更复杂的数据交互模式打下了坚实基础。在下一篇文章中,我们将转向 JavaScript 中的面向对象内容,探索如何更好地组织和管理你的代码。

更多推荐