假設我們有壹個複雜的對象,我們希望將其轉換爲字符串,以通過網絡發送,或者只是爲了在日志中輸出它。
當然,這樣的字符串應該包含所有重要的屬性。
我們可以像這樣實現轉換:
let user = { name: "John", age: 30, toString() { return `{name: "${this.name}", age: ${this.age}}`; } }; alert(user); // {name: "John", age: 30}
……但在開發過程中,會新增壹些屬性,舊的屬性會被重命名和刪除。每次更新這種 toString
都會非常痛苦。我們可以嘗試遍曆其中的屬性,但是如果對象很複雜,並且在屬性中嵌套了對象呢?我們也需要對它們進行轉換。
幸運的是,不需要編寫代碼來處理所有這些問題。這項任務已經解決了。
JSON(JavaScript Object Notation)是表示值和對象的通用格式。在 RFC 4627 標准中有對其的描述。最初它是爲 JavaScript 而創建的,但許多其他編程語言也有用于處理它的庫。因此,當客戶端使用 JavaScript 而服務器端是使用 Ruby/PHP/Java 等語言編寫的時,使用 JSON 可以很容易地進行數據交換。
JavaScript 提供了如下方法:
JSON.stringify
將對象轉換爲 JSON。
JSON.parse
將 JSON 轉換回對象。
例如,在這裏我們 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)
接收對象並將其轉換爲字符串。
得到的 json
字符串是壹個被稱爲 JSON 編碼(JSON-encoded) 或 序列化(serialized) 或 字符串化(stringified) 或 編組化(marshalled) 的對象。我們現在已經准備好通過有線發送它或將其放入普通數據存儲。
請注意,JSON 編碼的對象與對象字面量有幾個重要的區別:
字符串使用雙引號。JSON 中沒有單引號或反引號。所以 'John'
被轉換爲 "John"
。
對象屬性名稱也是雙引號的。這是強制性的。所以 age:30
被轉換成 "age":30
。
JSON.stringify
也可以應用于原始(primitive)數據類型。
JSON 支持以下數據類型:
Objects { ... }
Arrays [ ... ]
Primitives:
strings,
numbers,
boolean values true/false
,
null
。
例如:
// 數字在 JSON 還是數字 alert( JSON.stringify(1) ) // 1 // 字符串在 JSON 中還是字符串,只是被雙引號擴起來 alert( JSON.stringify('test') ) // "test" alert( JSON.stringify(true) ); // true alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]
JSON 是語言無關的純數據規範,因此壹些特定于 JavaScript 的對象屬性會被 JSON.stringify
跳過。
即:
函數屬性(方法)。
Symbol 類型的鍵和值。
存儲 undefined
的屬性。
let user = { sayHi() { // 被忽略 alert("Hello"); }, [Symbol("id")]: 123, // 被忽略 something: undefined // 被忽略 }; alert( JSON.stringify(user) ); // {}(空對象)
通常這很好。如果這不是我們想要的方式,那麽我們很快就會看到如何自定義轉換方式。
最棒的是支持嵌套對象轉換,並且可以自動對其進行轉換。
例如:
let meetup = { title: "Conference", room: { number: 23, participants: ["john", "ann"] } }; alert( JSON.stringify(meetup) ); /* 整個結構都被字符串化了 { "title":"Conference", "room":{"number":23,"participants":["john","ann"]}, } */
重要的限制:不得有循環引用。
例如:
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
在這裏,轉換失敗了,因爲循環引用:room.occupiedBy
引用了 meetup
,meetup.place
引用了 room
:
JSON.stringify
的完整語法是:
let json = JSON.stringify(value[, replacer, space])
value
要編碼的值。
replacer
要編碼的屬性數組或映射函數 function(key, value)
。
space
用于格式化的空格數量。
大部分情況,JSON.stringify
僅與第壹個參數壹起使用。但是,如果我們需要微調替換過程,比如過濾掉循環引用,我們可以使用 JSON.stringify
的第二個參數。
如果我們傳遞壹個屬性數組給它,那麽只有這些屬性會被編碼。
例如:
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":[{},{}]}
這裏我們可能過于嚴格了。屬性列表應用于了整個對象結構。所以 participants
是空的,因爲 name
不在列表中。
讓我們包含除了會導致循環引用的 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} } */
現在,除 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 引用了 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
函數會獲取每個鍵/值對,包括嵌套對象和數組項。它被遞歸地應用。replacer
中的 this
的值是包含當前屬性的對象。
第壹個調用很特別。它是使用特殊的“包裝對象”制作的:{"": meetup}
。換句話說,第壹個 (key, value)
對的鍵是空的,並且該值是整個目標對象。這就是上面的示例中第壹行是 ":[object Object]"
的原因。
這個理念是爲了給 replacer
提供盡可能多的功能:如果有必要,它有機會分析並替換/跳過整個對象。
JSON.stringify(value, replacer, spaces)
的第三個參數是用于優化格式的空格數量。
以前,所有字符串化的對象都沒有縮進和額外的空格。如果我們想通過網絡發送壹個對象,那就沒什麽問題。space
參數專門用于調整出更美觀的輸出。
這裏的 space = 2
告訴 JavaScript 在多行中顯示嵌套的對象,對象內部縮進 2 個空格:
let user = { name: "John", age: 25, roles: { isAdmin: false, isEditor: true } }; alert(JSON.stringify(user, null, 2)); /* 兩個空格的縮進: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */ /* 對于 JSON.stringify(user, null, 4) 的結果會有更多縮進: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */
第三個參數也可以是字符串。在這種情況下,字符串用于縮進,而不是空格的數量。
spaces
參數僅用于日志記錄和美化輸出。
像 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
添加壹個自定義的 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 字符串,我們需要另壹個方法 JSON.parse。
語法:
let value = JSON.parse(str, [reviver]);
str
要解析的 JSON 字符串。
reviver
可選的函數 function(key,value),該函數將爲每個 (key, value)
對調用,並可以對值進行轉換。
例如:
// 字符串化數組 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", // 錯誤:屬性名沒有雙引號 "surname": 'Smith', // 錯誤:值使用的是單引號(必須使用雙引號) 'isAdmin': false // 錯誤:鍵使用的是單引號(必須使用雙引號) "birthday": new Date(2000, 2, 3), // 錯誤:不允許使用 "new",只能是裸值 "friends": [0,1,2,3] // 這個沒問題 }`;
此外,JSON 不支持注釋。向 JSON 添加注釋無效。
還有另壹種名爲 JSON5 的格式,它允許未加引號的鍵,也允許注釋等。但這是壹個獨立的庫,不在語言的規範中。
標准 JSON 格式之所以如此嚴格,並不是因爲它的制定者們偷懶,而是爲了能夠簡單,可靠且快速地實現解析算法。
想象壹下,我們從服務器上獲得了壹個字符串化的 meetup
對象。
它看起來像這樣:
// title: (meetup title), date: (meetup date) let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
……現在我們需要對它進行 反序列(deserialize),把它轉換回 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
呢?
讓我們將 reviver 函數傳遞給 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() ); // 現在正常運行了!
順便說壹下,這也適用于嵌套對象:
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() ); // 正常運行了!
JSON 是壹種數據格式,具有自己的獨立標准和大多數編程語言的庫。
JSON 支持 object,array,string,number,boolean 和 null
。
JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify 和解析 JSON 的方法 JSON.parse。
這兩種方法都支持用于智能讀/寫的轉換函數。
如果壹個對象具有 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 }; // 循環引用 room.occupiedBy = meetup; meetup.self = meetup; alert( JSON.stringify(meetup, function replacer(key, value) { /* your code */ })); /* 結果應該是: { "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==""
以排除第壹個調用時 value
是 meetup
的情況。