JSON之所以流行,擁有與JavaScript類似的語法並不是全部原因。更重要的一個原因是,可以把JSON數據結構解析為有用的 JavaScript對象。與XML數據結構要解析成DOM文檔而且從中提取數據極為麻煩相比,JSON可以解析為JavaScript對象的優勢極其明 顯。
JSON對象
早期的JSON解析器基本上就是使用JavaScript的eval()函數。由於JSON是JavaScript語法的子集,因此eval()函 數可以解析、解釋並返回JavaScript對象和數組。ECMAScript 5對解析Json的行為進行規范,定義了全局對象JSON。支持這個對象的瀏覽器有IE8+、Firefox 3.5+、Safari4+、Chrome和Opera10.5+。對於較早版本的瀏覽器可以使用一個shim:https://github.com /douglascrockford/JSON-js。在舊版本的瀏覽器中,使用eval()對JSON數據結構求值存在風險,因為可能會執行一些惡意代 碼。對於不能原生支持JSON解析的瀏覽器,使用這個shim是最佳的選擇。
JSON對象有兩個方法:stringify()和parse()。在最簡單的情況下,這兩個方法分別用於把JavaScript對象序列化為JSON字符串和把JSON字符串解析為原生JavaScript值。例如:
var book = { title: "Professional JavaScript", authors: ["NIcholas C. Zakas"], edition: 3, year: 2011 }; var jsonText = JSON.stringify(book);
這個例子使用JSON.stringify()把一個JavaScript對象序列化為一個JSON字符串,然后將它保存在變量jsonText 中。默認情況下,JSON.stringify()輸出的JSON字符串不包含任何空格字符或縮進,因此保存在jsonText中的字符串如下所示:
{"title":"Professional JavaScript","authors":["NIcholas C. Zakas"],"edition":3,"year":2011}
在序列化JavaScript對象時,所有函數及原型成員都會被有意忽略,不體現在結果中。此時,值為undefined的任何屬性也都會被跳過。結果中最終都是值為有效JSON數據類型的實例屬性。
將JSON字符串直接傳遞給JSON.parse()就可以得到相應的JavaScript值。例如,使用下列代碼就可以創建與book類似的對象:
var bookCopy = JSON.parse(jsonText);
注意,雖然book與bookCopy具有相同的屬性,但它們是兩個對立的、沒有任何關系的對象。
如果傳給JSON.parse()的字符串不是有效的JSON,該方法會拋出錯誤。
JSON序列化選項
實際上,JSON.stringify()除了要序列化的JavaScript對象外,還可以接收另外兩個參數,這兩個參數用於指定以下不同的方式 序列化JavaScript對象。第一個參數是個過濾器,可以是一個數組,也可以是一個函數;第二個參數是一個選項,表示是否在JSON字符串中保留縮 進。單獨或組合使用這兩個參數,可以更全面深入地控制JSON的序列化。
1.過濾結果
如果過濾器參數是數組,那么JSON.stringify()結果中將只包含數組中列出的屬性。來看下面的例子。
var book = { "title": "Professional JavaScript", "authors": ["Nicholas C. Zakas"], edition: 3, year: 2011 }; var jsonText = JSON.stringify(book, ["title", "edition"]);
JSON.stringify()的第二個參數是一個數組,其中包含兩個字符串:“title”和“edition”。這個屬性將要序列化的對象中的屬性是對應的,因此在返回的結果字符串中,就只會包含這兩個屬性:
{"title":"Professional JavaScript", "edition":3}
如果第二個參數是函數,行為會稍有不同。傳入的函數接收兩個參數,屬性(鍵)名和屬性值。根據屬性(鍵)名可以知道應該如何處理要序列化的對象中的屬性。屬性名只能是字符串,而在值並非鍵值對兒結構的值時,鍵名可以是空字符串。
為了改變序列化對象的結果,函數返回的值就是相應鍵的值。不過要注意,如果函數返回了undefined,那么相應的屬性會被忽略。還是看一個例子吧。
var book = { "title": "Professional JavaScript", "authors": ["Nicholas C. Zakas"], edition: 3, year: 2011 }; var jsonText = JSON.stringify(book, function (key, value) { switch (key) { case "authors": return value.join(",") case "year": return 5000; case "edition": return undefined; default: return value; } }); alert(jsonText);
這里,函數過濾器根據傳入的鍵來決定結果。如果鍵為“authors”,就將數組連接為一個字符串;如果鍵為“year”,則將其值設置為 5000;如果鍵為“edition”,通過返回undefined刪除該屬性。最后,一定要提供default項,此時返回傳入的值,以便其它值都能正 常出現在結果中。實際上,第一次調用這個函數過濾器,傳入的鍵是一個空字符串,而值就是book對象。序列化后的JSON字符串如下所示:
{"title":"Professional JavaScript","authors":"Nicholas C. Zakas","year":5000}
要序列化的對象中的每一個對象都要經過過濾器,因此數組中的每個帶有這些屬性的對象經過過濾之后,每個對象都只會包含“title”、“authors”和“year”屬性。
2.字符串縮進
JSON.stringify()方法的第三個參數用於控制結果中的縮進和空白符。如果這個參數是一個數值,那它表示的是每個級別縮進的空格數。例如,要在每個級別縮進4個空格,可以這樣寫代碼:
var book = { "title": "Professional JavaScript", "authors": ["Nicholas C. Zakas"], edition: 3, year: 2011 }; var jsonText = JSON.stringify(book, null, 4);
保存在jsonText中的字符串如下所示:
{ "title": "Professional JavaScript", "authors": ["Nicholas C. Zakas"], "edition": 3, "year": 2011 }
SON.stringify()也在結果字符串中插入換行符以提高可讀性。只要傳入有效的控制縮進的參數值,結果字符串就會包含換行符。最大縮進空格數為10,所有大於10的值會自動轉換為10。
如果縮進參數是一個字符串而非數值,則這個字符串將在JSON字符串中被用作縮進字符。在使用字符串的情況下,可以將縮進字符設置為制表符,或者兩個短划線之類的任意字符。
var jsonText = JSON.stringify(book, null, " -- ");
這樣,jsonText中的字符串將變成如下所示:
{ --"title": "Professional JavaScript", --"authors": [----"Nicholas C. Zakas"--], --"edition": 3, --"year": 2011 }
縮進字符串最長不能超過10個字符長。如果字符串長度超過了10個,結果中只出現10個字符。
3.toJSON()方法
有時候,JSON.stringify()還是不能滿足對某些對象進行自定義序列化的需求。在這些情況下,可以通過對象上調用toJSON()方 法,返回其自身的JSON數據格式。原生Date對象有一個toJSON()方法,能夠將JavaScript的Date對象自動轉換成ISO8601日 期字符串(與在Date對象上調用toISOString()的結果完全一樣)。
可以為任何對象添加toJSON()方法,比如:
var book = { "title": "Professional JavaScript", "authors": ["Nicholas C. Zakas"], edition: 3, year: 2011, toJSON: function () { return this.title; } }; var jsonText = JSON.stringify(book);
以上代碼在book對象上定義了一個toJSON()方法,該方法返回圖書的書名。與Date對象類似,這個對象也將被序列化為一個簡單的字符串而 非對象。可以讓toJSON()方法返回任何序列化的值,它都能正常工作。也可以讓這個方法返回undefined,此時如果包含它的對象嵌入在另一個對 象中,會導致該對象的值變成null,而如果包含它的對象是頂級對象,結果就是undefined。
toJSON()可以作為函數過濾器的補充,因此理解序列化的內部順序十分重要。假設把一個對象傳入JSON.stringify(),序列化該對象的順序如下。
- 如果存在toJSON()方法而且能通過它取得有效的值,則調用該方法。否則,按默認順序執行序列化。
- 如果提供了第二個參數,應用這個函數過濾器。傳入函數過濾器的值是第(1)步返回的值。
- 對第(2)步返回的每個值進行相應的序列化。
- 如果提供了第三個參數,執行相應的格式化。
無論是考慮定義toJSON()方法,還是考慮使用函數過濾器,亦或需要同時使用兩者,理解這個順序都是至關重要的。
JSON解析選項
JSON.parse()方法也可以接收另一個參數,該參數是一個函數,將在每個鍵值對兒上調用。為了區別JSON.stringify()接收的 替換(過濾)函數,這個函數被稱為還原函數(reviver),但實際上這兩個函數的簽名是相同的——它們都接收兩個參數,一個鍵和一個值,而且都需要返 回一個值。
如果還原函數返回undefined,則表示要從結果中刪除相應的鍵;如果返回其它值,則將該值插入到結果中。在將日期字符串轉換為Date對象時,經常要用到還原函數。例如:
var book = { "title": "Professional JavaScript", "authors": ["Nicholas C. Zakas"], edition: 3, year: 2011, releaseDate: new Date(2011, 11, 1) }; var jsonText = JSON.stringify(book); var bookCopy = JSON.parse(jsonText, function (key, value) { if (key == "releaseDate") { return new Date(value); } else { return value; } }); alert(bookCopy.releaseDate.getFullYear());
以上代碼先是為book對象新增了一個releaseDate屬性,該屬性保存着一個Date對象。這個對象在經過序列化之后變成了有效的JSON 字符串,然后經過解析又在bookCopy中還原為一個Date對象。還原函數在遇到”releaseDate”鍵時,會基於相應的值創建一個新的 Date對象。結果就是bookCopy.releaseDate屬性中會保存一個Date對象。正是因為如此,才能基於這個對象調用 getFullYear()方法。