Javascript JSON 方法,toJSON

2023-02-17 10:49 更新

假設(shè)我們有一個復(fù)雜的對象,我們希望將其轉(zhuǎn)換為字符串,以通過網(wǎng)絡(luò)發(fā)送,或者只是為了在日志中輸出它。

當(dāng)然,這樣的字符串應(yīng)該包含所有重要的屬性。

我們可以像這樣實現(xiàn)轉(zhuǎn)換:

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

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

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

……但在開發(fā)過程中,會新增一些屬性,舊的屬性會被重命名和刪除。每次更新這種 toString 都會非常痛苦。我們可以嘗試遍歷其中的屬性,但是如果對象很復(fù)雜,并且在屬性中嵌套了對象呢?我們也需要對它們進(jìn)行轉(zhuǎn)換。

幸運(yùn)的是,不需要編寫代碼來處理所有這些問題。這項任務(wù)已經(jīng)解決了。

JSON.stringify

JSON(JavaScript Object Notation)是表示值和對象的通用格式。在 RFC 4627 標(biāo)準(zhǔn)中有對其的描述。最初它是為 JavaScript 而創(chuàng)建的,但許多其他編程語言也有用于處理它的庫。因此,當(dāng)客戶端使用 JavaScript 而服務(wù)器端是使用 Ruby/PHP/Java 等語言編寫的時,使用 JSON 可以很容易地進(jìn)行數(shù)據(jù)交換。

JavaScript 提供了如下方法:

  • ?JSON.stringify? 將對象轉(zhuǎn)換為 JSON。
  • ?JSON.parse? 將 JSON 轉(zhuǎn)換回對象。

例如,在這里我們 ?JSON.stringify? 一個 ?student? 對象:

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 編碼的對象:
{
  "name": "John",
  "age": 30,
  "isAdmin": false,
  "courses": ["html", "css", "js"],
  "spouse": null
}
*/

方法 JSON.stringify(student) 接收對象并將其轉(zhuǎn)換為字符串。

得到的 json 字符串是一個被稱為 JSON 編碼(JSON-encoded) 或 序列化(serialized) 或 字符串化(stringified) 或 編組化(marshalled) 的對象。我們現(xiàn)在已經(jīng)準(zhǔn)備好通過有線發(fā)送它或?qū)⑵浞湃肫胀〝?shù)據(jù)存儲。

請注意,JSON 編碼的對象與對象字面量有幾個重要的區(qū)別:

  • 字符串使用雙引號。JSON 中沒有單引號或反引號。所以 ?'John'? 被轉(zhuǎn)換為 ?"John"?。
  • 對象屬性名稱也是雙引號的。這是強(qiáng)制性的。所以 ?age:30? 被轉(zhuǎn)換成 ?"age":30?。

JSON.stringify 也可以應(yīng)用于原始(primitive)數(shù)據(jù)類型。

JSON 支持以下數(shù)據(jù)類型:

  • Objects ?{ ... }?
  • Arrays ?[ ... ]?
  • Primitives:
    • strings,
    • numbers,
    • boolean values ?true/false?,
    • ?null?。

例如:

// 數(shù)字在 JSON 還是數(shù)字
alert( JSON.stringify(1) ) // 1

// 字符串在 JSON 中還是字符串,只是被雙引號擴(kuò)起來
alert( JSON.stringify('test') ) // "test"

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

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

JSON 是語言無關(guān)的純數(shù)據(jù)規(guī)范,因此一些特定于 JavaScript 的對象屬性會被 JSON.stringify 跳過。

即:

  • 函數(shù)屬性(方法)。
  • Symbol 類型的鍵和值。
  • 存儲 ?undefined? 的屬性。
let user = {
  sayHi() { // 被忽略
    alert("Hello");
  },
  [Symbol("id")]: 123, // 被忽略
  something: undefined // 被忽略
};

alert( JSON.stringify(user) ); // {}(空對象)

通常這很好。如果這不是我們想要的方式,那么我們很快就會看到如何自定義轉(zhuǎn)換方式。

最棒的是支持嵌套對象轉(zhuǎn)換,并且可以自動對其進(jìn)行轉(zhuǎn)換。

例如:

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

alert( JSON.stringify(meetup) );
/* 整個結(jié)構(gòu)都被字符串化了
{
  "title":"Conference",
  "room":{"number":23,"participants":["john","ann"]},
}
*/

重要的限制:不得有循環(huán)引用。

例如:

let room = {
  number: 23
};

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

meetup.place = room;       // meetup 引用了 room
room.occupiedBy = meetup; // room 引用了 meetup

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

在這里,轉(zhuǎn)換失敗了,因為循環(huán)引用:room.occupiedBy 引用了 meetup,meetup.place 引用了 room


排除和轉(zhuǎn)換:replacer

JSON.stringify 的完整語法是:

let json = JSON.stringify(value[, replacer, space])

value

要編碼的值。

replacer

要編碼的屬性數(shù)組或映射函數(shù) ?function(key, value)?。

space

用于格式化的空格數(shù)量

大部分情況,?JSON.stringify? 僅與第一個參數(shù)一起使用。但是,如果我們需要微調(diào)替換過程,比如過濾掉循環(huán)引用,我們可以使用 ?JSON.stringify? 的第二個參數(shù)。

如果我們傳遞一個屬性數(shù)組給它,那么只有這些屬性會被編碼。

例如:

let room = {
  number: 23
};

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

room.occupiedBy = meetup; // room 引用了 meetup

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

這里我們可能過于嚴(yán)格了。屬性列表應(yīng)用于了整個對象結(jié)構(gòu)。所以 participants 是空的,因為 name 不在列表中。

讓我們包含除了會導(dǎo)致循環(huán)引用的 room.occupiedBy 之外的所有屬性:

let room = {
  number: 23
};

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

room.occupiedBy = meetup; // room 引用了 meetup

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

現(xiàn)在,除 occupiedBy 以外的所有內(nèi)容都被序列化了。但是屬性的列表太長了。

幸運(yùn)的是,我們可以使用一個函數(shù)代替數(shù)組作為 replacer。

該函數(shù)會為每個 (key,value) 對調(diào)用并返回“已替換”的值,該值將替換原有的值。如果值被跳過了,則為 undefined

在我們的例子中,我們可以為 occupiedBy 以外的所有內(nèi)容按原樣返回 value。為了 occupiedBy,下面的代碼返回 undefined

let room = {
  number: 23
};

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

room.occupiedBy = meetup; // room 引用了 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 函數(shù)會獲取每個鍵/值對,包括嵌套對象和數(shù)組項。它被遞歸地應(yīng)用。replacer 中的 this 的值是包含當(dāng)前屬性的對象。

第一個調(diào)用很特別。它是使用特殊的“包裝對象”制作的:{"": meetup}。換句話說,第一個 (key, value) 對的鍵是空的,并且該值是整個目標(biāo)對象。這就是上面的示例中第一行是 ":[object Object]" 的原因。

這個理念是為了給 replacer 提供盡可能多的功能:如果有必要,它有機(jī)會分析并替換/跳過整個對象。

格式化:space

JSON.stringify(value, replacer, spaces) 的第三個參數(shù)是用于優(yōu)化格式的空格數(shù)量。

以前,所有字符串化的對象都沒有縮進(jìn)和額外的空格。如果我們想通過網(wǎng)絡(luò)發(fā)送一個對象,那就沒什么問題。space 參數(shù)專門用于調(diào)整出更美觀的輸出。

這里的 space = 2 告訴 JavaScript 在多行中顯示嵌套的對象,對象內(nèi)部縮進(jìn) 2 個空格:

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

alert(JSON.stringify(user, null, 2));
/* 兩個空格的縮進(jìn):
{
  "name": "John",
  "age": 25,
  "roles": {
    "isAdmin": false,
    "isEditor": true
  }
}
*/

/* 對于 JSON.stringify(user, null, 4) 的結(jié)果會有更多縮進(jìn):
{
    "name": "John",
    "age": 25,
    "roles": {
        "isAdmin": false,
        "isEditor": true
    }
}
*/

第三個參數(shù)也可以是字符串。在這種情況下,字符串用于縮進(jìn),而不是空格的數(shù)量。

?spaces? 參數(shù)僅用于日志記錄和美化輸出。

自定義 “toJSON”

像 toString 進(jìn)行字符串轉(zhuǎn)換,對象也可以提供 toJSON 方法來進(jìn)行 JSON 轉(zhuǎn)換。如果可用,JSON.stringify 會自動調(diào)用它。

例如:

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) 變成了一個字符串。這是因為所有日期都有一個內(nèi)建的 toJSON 方法來返回這種類型的字符串。

現(xiàn)在讓我們?yōu)閷ο?nbsp;room 添加一個自定義的 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 既可以用于直接調(diào)用 JSON.stringify(room) 也可以用于當(dāng) room 嵌套在另一個編碼對象中時。

JSON.parse

要解碼 JSON 字符串,我們需要另一個方法 JSON.parse

語法:

let value = JSON.parse(str, [reviver]);

str

要解析的 JSON 字符串。

reviver

可選的函數(shù) function(key,value),該函數(shù)將為每個 ?(key, value)? 對調(diào)用,并可以對值進(jìn)行轉(zhuǎn)換。

例如:

// 字符串化數(shù)組
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 可能會非常復(fù)雜,對象和數(shù)組可以包含其他對象和數(shù)組。但是它們必須遵循相同的 JSON 格式。

以下是手寫 JSON 時的典型錯誤(有時我們必須出于調(diào)試目的編寫它):

let json = `{
  name: "John",                     // 錯誤:屬性名沒有雙引號
  "surname": 'Smith',               // 錯誤:值使用的是單引號(必須使用雙引號)
  'isAdmin': false                  // 錯誤:鍵使用的是單引號(必須使用雙引號)
  "birthday": new Date(2000, 2, 3), // 錯誤:不允許使用 "new",只能是裸值
  "friends": [0,1,2,3]              // 這個沒問題
}`;

此外,JSON 不支持注釋。向 JSON 添加注釋無效。

還有另一種名為 JSON5 的格式,它允許未加引號的鍵,也允許注釋等。但這是一個獨立的庫,不在語言的規(guī)范中。

常規(guī)的 JSON 格式嚴(yán)格,并不是因為它的開發(fā)者很懶,而是為了實現(xiàn)簡單,可靠且快速地實現(xiàn)解析算法。

使用 reviver

想象一下,我們從服務(wù)器上獲得了一個字符串化的 ?meetup? 對象。

它看起來像這樣:

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

……現(xiàn)在我們需要對它進(jìn)行 反序列(deserialize),把它轉(zhuǎn)換回 JavaScript 對象。

讓我們通過調(diào)用 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 怎么知道應(yīng)該將字符串轉(zhuǎn)換為 Date 呢?

讓我們將 reviver 函數(shù)傳遞給 JSON.parse 作為第二個參數(shù),該函數(shù)按照“原樣”返回所有值,但是 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() ); // 現(xiàn)在正常運(yùn)行了!

順便說一下,這也適用于嵌套對象:

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() ); // 正常運(yùn)行了!

總結(jié)

  • JSON 是一種數(shù)據(jù)格式,具有自己的獨立標(biāo)準(zhǔn)和大多數(shù)編程語言的庫。
  • JSON 支持 object,array,string,number,boolean 和 ?null?。
  • JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify 和解析 JSON 的方法 JSON.parse。
  • 這兩種方法都支持用于智能讀/寫的轉(zhuǎn)換函數(shù)。
  • 如果一個對象具有 ?toJSON?,那么它會被 ?JSON.stringify? 調(diào)用。

任務(wù)


將對象轉(zhuǎn)換為 JSON,然后再轉(zhuǎn)換回來

重要程度: 5

將 ?user? 轉(zhuǎn)換為 JSON,然后將其轉(zhuǎn)換回到另一個變量。

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

解決方案

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

let user2 = JSON.parse(JSON.stringify(user));

排除反向引用

重要程度: 5

在簡單循環(huán)引用的情況下,我們可以通過名稱排除序列化中違規(guī)的屬性。

但是,有時我們不能只使用名稱,因為它既可能在循環(huán)引用中也可能在常規(guī)屬性中使用。因此,我們可以通過屬性值來檢查屬性。

編寫 replacer 函數(shù),移除引用 meetup 的屬性,并將其他所有屬性序列化:

let room = {
  number: 23
};

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

// 循環(huán)引用
room.occupiedBy = meetup;
meetup.self = meetup;

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

/* 結(jié)果應(yīng)該是:
{
  "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=="" 以排除第一個調(diào)用時 value 是 meetup 的情況。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號