給急躁的程式設計師的 JavaScript(ES2022 版)
請支持這本書:購買捐款
(廣告,請不要封鎖。)

45 建立和解析 JSON (JSON)



JSON(「JavaScript 物件表示法」)是一種儲存格式,使用文字編碼資料。其語法是 JavaScript 表達式的子集。舉例來說,考慮儲存在檔案 `jane.json` 中的下列文字

{
  "first": "Jane",
  "last": "Porter",
  "married": true,
  "born": 1890,
  "friends": [ "Tarzan", "Cheeta" ]
}

JavaScript 有提供建立和解析 JSON 方法的全球命名空間物件 JSON

45.1 JSON 的發現與標準化

Douglas Crockford 在 2001 年於 json.org 發布了 JSON 規格。他解釋道

我發現了 JSON。我並未聲稱自己發明了 JSON,因為它早已存在於自然界中。我所做的是我找到了它,我為它命名,我描述了它的用途。我並未聲稱自己是第一個發現它的人;我知道在至少一年之前,還有其他人發現了它。我發現的最早的案例是,早在 1996 年,Netscape 的某個人就使用 JavaScript 陣列文字進行資料傳輸,這至少早於我想到這個點子五年。

後來,JSON 被標準化為 ECMA-404

45.1.1 JSON 的語法已凍結

引用 ECMA-404 標準

由於它非常簡單,因此預計 JSON 語法永遠不會改變。這賦予 JSON 作為基礎符號,極大的穩定性。

因此,JSON 永遠不會獲得改進,例如可選的尾隨逗號、註解或未加引號的鍵,而不管它們是否被認為是理想的。然而,這仍然為建立編譯為純 JSON 的 JSON 超集留有空間。

45.2 JSON 語法

JSON 包含 JavaScript 的以下部分

因此,您無法(直接)在 JSON 中表示循環結構。

45.3 使用 JSON API

全域命名空間物件 JSON 包含用於處理 JSON 資料的方法。

45.3.1 JSON.stringify(data, replacer?, space?)

.stringify() 將 JavaScript data 轉換為 JSON 字串。在本節中,我們忽略參數 replacer;它在 §45.4「自訂字串化和剖析」 中說明。

45.3.1.1 結果:單行文字

如果您只提供第一個引數,.stringify() 會傳回單行文字

assert.equal(
  JSON.stringify({foo: ['a', 'b']}),
  '{"foo":["a","b"]}' );
45.3.1.2 結果:縮排行的樹狀結構

如果您為 space 提供非負整數,則 .stringify() 會傳回一行或多行,並依據巢狀層級,每層縮排 space 個空格

assert.equal(
JSON.stringify({foo: ['a', 'b']}, null, 2),
`{
  "foo": [
    "a",
    "b"
  ]
}`);
45.3.1.3 JavaScript 資料如何字串化的詳細資料

原始值

物件

45.3.2 JSON.parse(text, reviver?)

.parse() 會將 JSON text 轉換為 JavaScript 值。在此部分,我們會忽略參數 reviver;它會在 §45.4「自訂字串化和剖析」 中說明。

以下是使用 .parse() 的範例

> JSON.parse('{"foo":["a","b"]}')
{ foo: [ 'a', 'b' ] }

45.3.3 範例:轉換為 JSON 和從 JSON 轉換

下列類別實作從 (A 行) 和到 (B 行) JSON 的轉換。

class Point {
  static fromJson(jsonObj) { // (A)
    return new Point(jsonObj.x, jsonObj.y);
  }

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  
  toJSON() { // (B)
    return {x: this.x, y: this.y};
  }
}

  練習:將物件轉換為 JSON 和從 JSON 轉換

exercises/json/to_from_json_test.mjs

45.4 自訂字串化和剖析 (進階)

字串化和剖析可以自訂如下

45.4.1 .stringfy():指定要字串化的物件屬性

如果 .stringify() 的第二個參數是陣列,則只有在那裡提到的名稱的物件屬性會包含在結果中

const obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  }
};
assert.equal(
  JSON.stringify(obj, ['b', 'c']),
  '{"b":{"c":2}}');

45.4.2 .stringify().parse():值訪客

我所謂的值訪客是一個轉換 JavaScript 資料的函式

在此部分,JavaScript 資料被視為值的樹狀結構。如果資料是原子的,它就是只有根節點的樹狀結構。樹狀結構中的所有值會一次一個傳送給值訪客。根據訪客傳回的內容,目前的會被省略、變更或保留。

值訪客具有下列類型簽章

type ValueVisitor = (key: string, value: any) => any;

參數為

值訪客可以傳回

45.4.3 範例:拜訪值

以下程式碼顯示值訪客看到值的順序

const log = [];
function valueVisitor(key, value) {
  log.push({this: this, key, value});
  return value; // no change
}

const root = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  }
};
JSON.stringify(root, valueVisitor);
assert.deepEqual(log, [
  { this: { '': root }, key: '',  value: root   },
  { this: root        , key: 'a', value: 1      },
  { this: root        , key: 'b', value: root.b },
  { this: root.b      , key: 'c', value: 2      },
  { this: root.b      , key: 'd', value: 3      },
]);

正如我們所見,JSON.stringify() 的替換器由上而下拜訪值(根節點優先,葉節點最後)。選擇這種順序的理由是,我們正在將 JavaScript 值轉換為 JSON 值。單一 JavaScript 物件可能會擴充為 JSON 相容值的樹狀結構。

相反地,JSON.parse() 的復原器由下而上拜訪值(葉節點優先,根節點最後)。選擇這種順序的理由是,我們正在將 JSON 值組裝成 JavaScript 值。因此,我們需要先轉換各部分,才能轉換整體。

45.4.4 範例:將不受支援的值字串化

JSON.stringify() 不特別支援正規表示式物件,而是將它們當成一般物件來字串化

const obj = {
  name: 'abc',
  regex: /abc/ui,
};
assert.equal(
  JSON.stringify(obj),
  '{"name":"abc","regex":{}}');

我們可以透過替換器來修正這個問題

function replacer(key, value) {
  if (value instanceof RegExp) {
    return {
      __type__: 'RegExp',
      source: value.source,
      flags: value.flags,
    };
  } else {
    return value; // no change
  }
}
assert.equal(
JSON.stringify(obj, replacer, 2),
`{
  "name": "abc",
  "regex": {
    "__type__": "RegExp",
    "source": "abc",
    "flags": "iu"
  }
}`);

45.4.5 範例:剖析不受支援的值

要讓 JSON.parse() 剖析前一節的結果,我們需要一個復原器

function reviver(key, value) {
  // Very simple check
  if (value && value.__type__ === 'RegExp') {
    return new RegExp(value.source, value.flags);
  } else {
    return value;
  }
}
const str = `{
  "name": "abc",
  "regex": {
    "__type__": "RegExp",
    "source": "abc",
    "flags": "iu"
  }
}`;
assert.deepEqual(
  JSON.parse(str, reviver),
  {
    name: 'abc',
    regex: /abc/ui,
  });

45.5 常見問題

45.5.1 為什麼 JSON 不支援註解?

Douglas Crockford 在 2012 年 5 月 1 日的 Google+ 貼文中 解釋了原因

我從 JSON 中移除註解,因為我看到人們使用它們來保留剖析指令,這種做法會破壞互通性。我知道缺少註解會讓一些人感到難過,但這不應該如此。

假設你使用 JSON 來保留設定檔,而你想要為其加上註解。繼續,插入你喜歡的任何註解。然後在將其傳遞給 JSON 剖析器之前,透過 JSMin [JavaScript 的壓縮器] 進行處理。