2024 年 1 月 24 日

JSON 方法,toJSON

假設我們有一個複雜的物件,我們想要將它轉換成字串,以便透過網路傳送,或僅是為了記錄目的而輸出它。

當然,這樣的字串應該包含所有重要的屬性。

我們可以像這樣實作轉換

let user = {
  name: "John",
  age: 30,

  toString() {
    return `{name: "${this.name}", age: ${this.age}}`;
  }
};

alert(user); // {name: "John", age: 30}

…但在開發過程中,會新增新的屬性,舊的屬性會重新命名並移除。每次更新這樣的 toString 都會變成一種痛苦。我們可以嘗試在其中迴圈屬性,但如果物件很複雜,而且屬性中有巢狀物件怎麼辦?我們也需要實作它們的轉換。

幸運的是,無需撰寫程式碼來處理所有這些。此任務已解決。

JSON.stringify

JSON(JavaScript 物件表示法)是一種通用的格式,用於表示值和物件。它在 RFC 4627 標準中描述。最初它是為 JavaScript 製作的,但許多其他語言也有函式庫來處理它。因此,當客戶端使用 JavaScript 而伺服器以 Ruby/PHP/Java/Whatever 編寫時,很容易使用 JSON 進行資料交換。

JavaScript 提供方法

  • JSON.stringify 將物件轉換為 JSON。
  • JSON.parse 將 JSON 轉換回物件。

例如,這裡我們 JSON.stringify 一個學生

let student = {
  name: 'John',
  age: 30,
  isAdmin: false,
  courses: ['html', 'css', 'js'],
  spouse: null
};

let json = JSON.stringify(student);

alert(typeof json); // we've got a string!

alert(json);
/* JSON-encoded object:
{
  "name": "John",
  "age": 30,
  "isAdmin": false,
  "courses": ["html", "css", "js"],
  "spouse": null
}
*/

方法 JSON.stringify(student) 取得物件並將其轉換為字串。

產生的 json 字串稱為JSON 編碼序列化字串化封送的物件。我們準備好透過網路傳送它或將其放入純粹資料儲存中。

請注意,JSON 編碼物件與物件文字有幾個重要的差異

  • 字串使用雙引號。JSON 中沒有單引號或反引號。因此 'John' 變成 "John"
  • 物件屬性名稱也使用雙引號。這是強制性的。因此 age:30 變成 "age":30

JSON.stringify 也可套用於基本型別。

JSON 支援下列資料類型

  • 物件 { ... }
  • 陣列 [ ... ]
  • 基本型別
    • 字串,
    • 數字,
    • 布林值 true/false
    • null.

例如

// a number in JSON is just a number
alert( JSON.stringify(1) ) // 1

// a string in JSON is still a string, but double-quoted
alert( JSON.stringify('test') ) // "test"

alert( JSON.stringify(true) ); // true

alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]

JSON 是僅限資料的語言非依賴性規格,因此 JSON.stringify 會略過一些特定於 JavaScript 的物件屬性。

  • 函式屬性(方法)。
  • 符號鍵和值。
  • 儲存 undefined 的屬性。
let user = {
  sayHi() { // ignored
    alert("Hello");
  },
  [Symbol("id")]: 123, // ignored
  something: undefined // ignored
};

alert( JSON.stringify(user) ); // {} (empty object)

通常這樣就可以了。如果這不是我們想要的,那麼我們很快就會看到如何自訂這個程序。

很棒的是,它支援並自動轉換巢狀物件。

例如

let meetup = {
  title: "Conference",
  room: {
    number: 23,
    participants: ["john", "ann"]
  }
};

alert( JSON.stringify(meetup) );
/* The whole structure is stringified:
{
  "title":"Conference",
  "room":{"number":23,"participants":["john","ann"]},
}
*/

重要的限制:不能有循環參考。

例如

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: ["john", "ann"]
};

meetup.place = room;       // meetup references room
room.occupiedBy = meetup; // room references meetup

JSON.stringify(meetup); // Error: Converting circular structure to JSON

在此,轉換失敗,因為循環參考:room.occupiedBy 參考 meetup,而 meetup.place 參考 room

排除與轉換:替換器

JSON.stringify 的完整語法為

let json = JSON.stringify(value[, replacer, space])
要編碼的值。
替換器
要編碼的屬性陣列或對應函式 function(key, value)
空格
用於格式化的空格數量

大多數時候,JSON.stringify 只會使用第一個參數。但如果我們需要微調替換程序,例如過濾循環參照,我們可以使用 JSON.stringify 的第二個參數。

如果我們傳遞一個屬性陣列給它,則只會編碼這些屬性。

例如

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}

這裡我們可能太嚴格了。屬性清單套用於整個物件結構。因此 participants 中的物件是空的,因為清單中沒有 name

讓我們在清單中包含每個屬性,除了會導致循環參照的 room.occupiedBy

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
  "title":"Conference",
  "participants":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/

現在除了 occupiedBy 之外,一切都已序列化。但屬性清單很長。

幸運的是,我們可以使用函式而不是陣列作為 replacer

函式將針對每個 (key, value) 對呼叫,並應傳回「替換」值,該值將用於取代原始值。或者如果要略過該值,則傳回 undefined

在我們的案例中,我們可以對除了 occupiedBy 之外的所有內容傳回「原樣」的 value。若要忽略 occupiedBy,以下程式碼會傳回 undefined

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup references room
};

room.occupiedBy = meetup; // room references meetup

alert( JSON.stringify(meetup, function replacer(key, value) {
  alert(`${key}: ${value}`);
  return (key == 'occupiedBy') ? undefined : value;
}));

/* key:value pairs that come to replacer:
:             [object Object]
title:        Conference
participants: [object Object],[object Object]
0:            [object Object]
name:         John
1:            [object Object]
name:         Alice
place:        [object Object]
number:       23
occupiedBy: [object Object]
*/

請注意,replacer 函式會取得每個鍵/值對,包括巢狀物件和陣列項目。它會遞迴套用。replacer 內部的 this 值是包含目前屬性的物件。

第一次呼叫很特別。它是使用特殊「包裝物件」進行的:{"": meetup}。換句話說,第一個 (key, value) 對有一個空鍵,而值是整個目標物件。這就是為什麼在上面的範例中,第一行是 ":[object Object]"

這個想法是盡可能為 replacer 提供更多功能:它有機會分析和替換/略過整個物件,如果需要的話。

格式化:空格

JSON.stringify(value, replacer, space) 的第三個參數是要用於美化格式化的空格數量。

先前,所有字串化的物件都沒有縮排和額外的空格。如果我們要透過網路傳送物件,這樣很好。space 參數專門用於美觀的輸出。

這裡 space = 2 告訴 JavaScript 在多行上顯示巢狀物件,物件內部縮排 2 個空格

let user = {
  name: "John",
  age: 25,
  roles: {
    isAdmin: false,
    isEditor: true
  }
};

alert(JSON.stringify(user, null, 2));
/* two-space indents:
{
  "name": "John",
  "age": 25,
  "roles": {
    "isAdmin": false,
    "isEditor": true
  }
}
*/

/* for JSON.stringify(user, null, 4) the result would be more indented:
{
    "name": "John",
    "age": 25,
    "roles": {
        "isAdmin": false,
        "isEditor": true
    }
}
*/

第三個參數也可以是字串。在這種情況下,字串會用於縮排,而不是空格數。

space 參數僅用於記錄和美觀輸出的目的。

自訂「toJSON」

就像用於字串轉換的 toString,物件可以提供 toJSON 方法進行轉換為 JSON。如果可用,JSON.stringify 會自動呼叫它。

例如

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  date: new Date(Date.UTC(2017, 0, 1)),
  room
};

alert( JSON.stringify(meetup) );
/*
  {
    "title":"Conference",
    "date":"2017-01-01T00:00:00.000Z",  // (1)
    "room": {"number":23}               // (2)
  }
*/

在這裡,我們可以看到 date (1) 變成字串了。這是因為所有日期都有內建的 toJSON 方法,會傳回此類型的字串。

現在,讓我們為我們的物件 room (2) 新增自訂的 toJSON

let room = {
  number: 23,
  toJSON() {
    return this.number;
  }
};

let meetup = {
  title: "Conference",
  room
};

alert( JSON.stringify(room) ); // 23

alert( JSON.stringify(meetup) );
/*
  {
    "title":"Conference",
    "room": 23
  }
*/

正如我們所見,toJSON 用於直接呼叫 JSON.stringify(room),以及當 room 巢狀在另一個編碼物件中時。

JSON.parse

要解碼 JSON 字串,我們需要另一個名為 JSON.parse 的方法。

語法

let value = JSON.parse(str[, reviver]);
str
要解析的 JSON 字串。
reviver
可選的函式 (key,value),將會對每個 (key, value) 配對呼叫,並可以轉換值。

例如

// stringified array
let numbers = "[0, 1, 2, 3]";

numbers = JSON.parse(numbers);

alert( numbers[1] ); // 1

或用於巢狀物件

let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';

let user = JSON.parse(userData);

alert( user.friends[1] ); // 1

JSON 可能會複雜到必要程度,物件和陣列可以包含其他物件和陣列。但它們必須遵守相同的 JSON 格式。

以下是手寫 JSON 中常見的錯誤(有時我們必須為了除錯目的而寫它)

let json = `{
  name: "John",                     // mistake: property name without quotes
  "surname": 'Smith',               // mistake: single quotes in value (must be double)
  'isAdmin': false                  // mistake: single quotes in key (must be double)
  "birthday": new Date(2000, 2, 3), // mistake: no "new" is allowed, only bare values
  "friends": [0,1,2,3]              // here all fine
}`;

此外,JSON 不支援註解。在 JSON 中新增註解會使它無效。

還有一個名為 JSON5 的格式,允許未加引號的鍵、註解等。但這是一個獨立的函式庫,不在語言規格中。

常規 JSON 如此嚴格,並不是因為它的開發人員很懶,而是為了允許解析演算法容易、可靠且非常快速地實作。

使用 reviver

想像一下,我們從伺服器取得一個字串化的 meetup 物件。

它看起來像這樣

// title: (meetup title), date: (meetup date)
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

…現在我們需要將它「反序列化」,轉回 JavaScript 物件。

讓我們呼叫 JSON.parse 來執行此動作

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str);

alert( meetup.date.getDate() ); // Error!

糟糕!發生錯誤了!

meetup.date 的值是字串,而不是 Date 物件。JSON.parse 怎麼知道它應該將該字串轉換為 Date

讓我們將復活函式作為第二個引數傳遞給 JSON.parse,它會將所有值「原樣」傳回,但 date 會變成 Date

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( meetup.date.getDate() ); // now works!

順帶一提,這也適用於巢狀物件

let schedule = `{
  "meetups": [
    {"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
    {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
  ]
}`;

schedule = JSON.parse(schedule, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( schedule.meetups[1].date.getDate() ); // works!

摘要

  • JSON 是一種資料格式,它有自己的獨立標準,且支援大部分程式語言的函式庫。
  • JSON 支援一般物件、陣列、字串、數字、布林值和 null
  • JavaScript 提供方法 JSON.stringify 將資料序列化成 JSON,以及 JSON.parse 從 JSON 讀取資料。
  • 這兩個方法都支援轉換器函式,以進行智慧化讀取/寫入。
  • 如果物件有 toJSON,則 JSON.stringify 會呼叫它。

任務

重要性:5

user 轉換成 JSON,然後再讀回另一個變數。

let user = {
  name: "John Smith",
  age: 35
};
let user = {
  name: "John Smith",
  age: 35
};

let user2 = JSON.parse(JSON.stringify(user));
重要性:5

在環狀參照的簡單情況中,我們可以透過名稱從序列化中排除有問題的屬性。

但有時我們無法只使用名稱,因為它可能同時用於環狀參照和一般屬性。因此,我們可以透過其值來檢查屬性。

撰寫 replacer 函式,將所有內容轉換成字串,但移除參照 meetup 的屬性

let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  occupiedBy: [{name: "John"}, {name: "Alice"}],
  place: room
};

// circular references
room.occupiedBy = meetup;
meetup.self = meetup;

alert( JSON.stringify(meetup, function replacer(key, value) {
  /* your code */
}));

/* result should be:
{
  "title":"Conference",
  "occupiedBy":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/
let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  occupiedBy: [{name: "John"}, {name: "Alice"}],
  place: room
};

room.occupiedBy = meetup;
meetup.self = meetup;

alert( JSON.stringify(meetup, function replacer(key, value) {
  return (key != "" && value == meetup) ? undefined : value;
}));

/*
{
  "title":"Conference",
  "occupiedBy":[{"name":"John"},{"name":"Alice"}],
  "place":{"number":23}
}
*/

在這裡,我們也需要測試 key=="",以排除第一個呼叫,其中 valuemeetup 是正常的。

教學地圖

留言

留言前請先閱讀…
  • 如果您有改進建議,請 提交 GitHub 議題 或提出 pull request,而不是留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 若要插入少量的程式碼,請使用 <code> 標籤,若要插入多行,請將它們包在 <pre> 標籤中,若要插入 10 行以上的程式碼,請使用沙盒(plnkrjsbincodepen…)