引言
曾經(jīng)有一段時間,XML是互聯(lián)網(wǎng)上傳輸結(jié)構(gòu)化數(shù)據(jù)的事實標(biāo)準(zhǔn)。Web服務(wù)的第一次浪潮很大程度上都是建立在XML之上的,突出的特點是服務(wù)器與服務(wù)器間通信。然而,業(yè)界一直不乏質(zhì)疑XML的聲音。不少人認(rèn)為XML過于煩瑣、冗長。為解決這個問題,也涌現(xiàn)了一些方案。不過,Web的發(fā)展方向已經(jīng)改變了。
JSON是JavaScript的一個嚴(yán)格的子集,利用了JavaScript中的一些模式來表示結(jié)構(gòu)化數(shù)據(jù),它是一種數(shù)據(jù)格式,不是編程語言!
雖然 具有相同的語法形式,但JSON并不從屬于JavaScript,而且,并不是只有JavaScript才能使用JSON,畢竟JSON只是一種數(shù)據(jù)格式。很多編程語言都有針對JSON的解析器和序列化器。
語法
JSON的語法可以表示以下三種類型的值。
-
簡單值
使用與JavaScript相同的語法,可以在JSON中表示字符串、數(shù)值、布爾值和null。但JSON不支持JavaScript中的特殊值undefined。
-
對象
對象作為一種復(fù)雜數(shù)據(jù)類型,表示的是一組無序的鍵值對(key-value)。而每個鍵值對中的值可以是簡單值,也可以是復(fù)雜類型的值。
-
數(shù)組
數(shù)組也是一種復(fù)雜數(shù)據(jù)類型,表示一組有序的值的列表,可以通過數(shù)值索引來訪問其中的值。數(shù)組的值也可以是任意類型——簡單之、對象和數(shù)組。
JSON不支持變量、函數(shù)或者對象實例,它就是一種表示結(jié)構(gòu)化數(shù)據(jù)的格式,雖然與JavaScript中表示數(shù)據(jù)的某些語法相同,但它并不局限于JavaScript范疇。
簡單值
最簡單的JSON數(shù)據(jù)形式就是簡單值。例如,下面這個值是有效的JSON數(shù)據(jù):
5
這是JSON表示數(shù)值5的方式。類似地,下面是JSON表示字符串的方式:
"hello world!"
JavaScript字符串與JSON字符串最大區(qū)別在于:JSON字符串必須使用雙引號(單引號導(dǎo)致語法錯誤)。
布爾值和null也是有效的JSON形式。但是,在實際應(yīng)用中,JSON更多地用來表示更復(fù)雜的數(shù)據(jù)結(jié)構(gòu),而簡單值只是整個數(shù)據(jù)結(jié)構(gòu)中一部分。
對象
JSON中的對象與JavaScript字面量稍微有些不同,下面是一個JavaScript中的對象字面量:
var person = {
name:"Nicholas",
age:29
}
這是JavaScript創(chuàng)建對象字面量的標(biāo)準(zhǔn)方式,但JSON中的對象要求給屬性加引號。JSON表示上述對象方式如下:
{
"name":"Nicholas",
"age":29
}
與JavaScript的對象字面量相比,JSON對象有兩個地方不同:
-
沒有聲明變量(JSON中沒有變量的概念)。
-
沒有末尾分號(因為不是JavaScript語句,所以沒有分號)
但是!對象的屬性必須有雙引號!這在JSON中是必須的。
屬性值可以是簡單值,也可以是復(fù)雜類型值,因此可以這樣:
{
"name": "Nicholas",
"age": 29,
"school": {
"name": "Merrimack College",
"location": "North Andover, MA>"
}
}
這個例子在頂級對象中嵌入了學(xué)校("school")信息。雖然有兩個name屬性,但由于它們分別屬于不同對象,因此這樣完全沒有問題。不過,同一對象下絕對不應(yīng)該出現(xiàn)兩個同名屬性。
與JavaScript不同,JSON中對象的屬性名任何時候都必須加雙引號。手工編寫JSON時,忘了給對象屬性名加雙引號或者把雙引號寫成單引號都是常見的錯誤。
數(shù)組
JSON中的第二種復(fù)雜數(shù)據(jù)類型是數(shù)組。。JSON數(shù)組采用的就是JavaScript中的數(shù)組字面量形式。
JavaScript中的數(shù)組字面量:
var values = [25,"Hi",true];
JSON中:
[25,"Hi",true]
對的,就是沒有了變量和分號的概念,其他和JavaScript是類似的。
把數(shù)組和對象結(jié)合起來,可以構(gòu)成更復(fù)雜的數(shù)據(jù)集合:
[
{
"title":"Professional JavaScript",
"authors":[
"Nicholas C. zakas"
],
"editons":3,
"year":2011
},
{
"title":"Professional JavaScript",
"authors":[
"Nicholas C. zakas"
],
"editons":2,
"year":2009
},
{
"title":"Professional Ajax",
"authors":[
"Nicholas C. zakas",
"JereMy Mcpeak",
"Joe Fawcett"
],
"editons":2,
"year":2008
},
{
"title":"Professional Ajax",
"authors":[
"Nicholas C. zakas",
"JereMy Mcpeak",
"Joe Fawcett"
],
"editons":1,
"year":2007
},
{
"title":"Professional Javascript",
"authors":[
"Nicholas C. zakas"
],
"editons":1,
"year":2006
}
]
這個數(shù)組中包含一些圖書的對象。每個對象都有幾個屬性,其中一個屬性是"authors",這個屬性的值又是一個數(shù)組。對象和數(shù)組通常是JSON數(shù)據(jù)結(jié)構(gòu)的最外層形式(當(dāng)然這不是強(qiáng)制規(guī)定的),利用它們可以創(chuàng)造出各種各樣的數(shù)據(jù)結(jié)構(gòu)。
解析與序列化(重)
JSON之所以流行,擁有與JavaScript類似的語法并不是全部原因。更重要的一個原因是,可以把JSON數(shù)據(jù)結(jié)構(gòu)解析為有用的JavaScript對象。與XML數(shù)據(jù)結(jié)構(gòu)要解析成DOM文檔而且從中提取數(shù)據(jù)極為麻煩相比,JSON可以解析為JavaScript對象的優(yōu)勢極其明顯。就以上一節(jié)中包含一組圖書的JSON數(shù)據(jù)結(jié)構(gòu)為例,在解析為JavaScript對象后,只需要下面一行簡單的代碼就可以取得第三本書的書名:
book[2].title
當(dāng)然,這是是假設(shè)解析JSON數(shù)據(jù)結(jié)構(gòu)后得到的對象保存到了變量books中。再看看下面在DOM結(jié)構(gòu)中查找數(shù)據(jù)的代碼:
doc.getElementsByTagName("book")[2].getAttribute("title")
看看這些多余的方法調(diào)用,就不難理解為什么JSON能得到JavaScript開發(fā)人員的歡迎了。從此以后,JSON就成為WEB服務(wù)開發(fā)中交換數(shù)據(jù)的事實標(biāo)準(zhǔn)。
JSON對象
早期的JSON解析器基本上就是使用JavaScript的eval()函數(shù)。由于JSON是JavaScript語法的子集,因此eval()可以解析、解釋并返回JavaScript對象和數(shù)組。ES5對解析JSON的行為進(jìn)行規(guī)范,定義了全局對象JSON。支持這個對象的瀏覽器有IE8+、Firefox3.5+、Safari4+、Chrome和Opera 10.5+。
JSON對象有兩個方法:stringify()
和parse()
。在最簡單的情況下,這兩個方法分別用于把JavaScript對象序列化為JSON字符串和把JSON字符串解析為原生JavaScript值。例如:
var book = {
title: "Professional Javascript",
authors: [
"Nicholas C. Zakas"
],
eidtion: 3,
year: 2011
};
var jsonText = JSON.stringify(book);
console.log(jsonText);
這個例子使用JSON.stringify()
把一個JavaScript對象序列化為一個JSON字符串,然后將它保存在jsonText中。默認(rèn)情況下,JSON.stringify()
輸出的JSON字符串不包含任何任何空格字符或縮進(jìn),因此保存在jsonText中的字符串如下:
{"title":"Professional Javascript","authors":["Nicholas C. Zakas"],"eidtion":3,"year":2011}
在序列化JavaScript對象時,所有函數(shù)及原型成員都會有意被忽略,不體現(xiàn)在結(jié)果中。此外,值為undefined的任何屬性也都會跳過。結(jié)果中最終都是值為有效JSON數(shù)據(jù)類型的實例屬性。
將JSON字符串直接傳遞給JSON.parse()
就可以得到相應(yīng)的JavaScript值。使用下列代碼就可以創(chuàng)建與類似book類似的對象:
var bookCopy = JSON.parse(jsonText);
console.log(bookCopy["edition"]); // 3
注意,雖然book與bookCopy具有相同的屬性,但它們是兩個獨立的、沒有任何關(guān)系的對象。
如果傳給JSON.parse()的字符串不是有效的JSON,該方法會拋出錯誤。
序列化選項
實際上,JSON.stringify()除了要序列化JavaScript對象外,還可以接收另外兩個參數(shù),這兩個參數(shù)用于指定以不同的方式序列化JavaScript對象。第一個參數(shù)是個過濾器,可以是一個數(shù)組,也可以是一個函數(shù);第二個參數(shù)是一個選項,表示是否在JSON字符串中保留縮進(jìn)。單獨或組合使用這兩個參數(shù),可以更全面深入地控制JSON的序列化。
1. 過濾結(jié)果
如果過濾器參數(shù)是數(shù)組,那么JSON.stringify()的結(jié)果中將只包含數(shù)組中列出的屬性。來看下面的例子。
var book = {
"title":"Professional Javascript",
"authors":[
"Nicholas C. Zakas"
],
edition:3,
year:2011
};
var jsonText = JSON.stringify(book,["title","edition"]);
console.log(jsonText); // {"title":"Professional Javascript","edition":3}
JSON.stringify()
的第二個參數(shù)是一個數(shù)組,其中包含兩個字符串:"title"
和"edition"
。這兩個屬性與將要序列化的對象中的屬性是對應(yīng)的,因此在返回的結(jié)果字符串中,就只會包含這兩個屬性:
{"title":"Professional Javascript","edition":3}
如果第二個參數(shù)是函數(shù),行為會稍有不同。傳入的函數(shù)接收兩個參數(shù),屬性名和屬性值。根據(jù)屬性名可以知道應(yīng)該如何處理要序列化的對象中的屬性。屬性名只能是字符串,而在值不是鍵值對結(jié)構(gòu)的情況下,鍵名可以是空字符串。
為了改變序列化對象的結(jié)果,函數(shù)返回的值就是相應(yīng)鍵的值。不過要注意,如果函數(shù)返回了undefined,那么相應(yīng)的屬性會被忽略。還是看一個例子吧。
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;
}
});
這里,函數(shù)過濾器根據(jù)傳入的鍵來決定結(jié)果。如果鍵為"authors",就將數(shù)組連接為一個字符串;如果鍵為"year",則將其值設(shè)置為5000;如果鍵為"edition",通過返回undefined刪除該屬性。最后,一定要提供default項,此時返回傳入的值,以便其他值都能正常出現(xiàn)在結(jié)果中。
實際上,第一次調(diào)用函數(shù)過濾器,傳入的鍵是一個空字符串,而值就是book對象。序列化后的JSON字符串付下所示:
{"title":"Professional Javascript","authors":"Nicholas C. Zakas","year":5000}
要序列化的對象中的每一個對象都要經(jīng)過過濾器,因此數(shù)組中的每個帶有這些屬性的對象經(jīng)過過濾器之后,每個對象都只包含"title"、"authors"和"year"屬性。
2. 字符串縮進(jìn)
JSON.stringify()
方法的第三個參數(shù)用于控制結(jié)果中的縮進(jìn)和空白符。如果這個參數(shù)是一個數(shù)值,那它表示的是每個級別縮進(jìn)的空格數(shù)。例如:要在每個級別縮進(jìn)4個空格,可以這樣:
var book = {
"title": "Professional Javascript",
"authors": [
"Nicholas C. Zakas"
],
edition: 3,
year: 2011
}
var jsonText = JSON.stringify(book, null, 4);
/*
jsonText
{
fuck.js:11
"title": "Professional Javascript",
"authors": [
"Nicholas C. Zakas"
],
"edition": 3,
"year": 2011
}
*/
不知道讀者注意到?jīng)]有,JSON.stringify()也在結(jié)果字符串中插入了換行符以提高可讀性。只要傳入有效的控制縮進(jìn)的參數(shù)值,結(jié)果字符串就會包含換行符。(只縮進(jìn)而不換行意義不大)最大縮進(jìn)空格數(shù)為10,所有大于10的值都會自動轉(zhuǎn)換為10。
如果縮進(jìn)參數(shù)是一個字符串而非數(shù)值,則這個字符串將在JSON字符串中被用作縮進(jìn)字符(不在使用空格)。在使用字符串的情況下,可以將縮進(jìn)字符設(shè)置為制表符,或者兩個短劃線之類的任意字符。
var jsonText = JSON.stringify(book,null," - -");
這樣,jsonText中的字符串將變成如下所示:
var book = {
"title": "Professional Javascript",
"authors": [
"Nicholas C. Zakas"
],
edition: 3,
year: 2011
}
var jsonText = JSON.stringify(book,null," - -");
console.log(jsonText);
/*
{
fuck.js:11
- -"title": "Professional Javascript",
- -"authors": [
- - - -"Nicholas C. Zakas"
- -],
- -"edition": 3,
- -"year": 2011
}
*/
縮進(jìn)字符串最長不能超過10個字符長。如果字符串長度超過了10個,結(jié)果中將只出現(xiàn)前10個字符。
3. toJSON()方法
有時候,JSON.stringify()還是不能滿足對某些對象進(jìn)行自定義序列化的需求。在這些情況下,可以給對象定義toJSON()方法,返回其自身的JSON數(shù)據(jù)格式。原生Date對象有一個toJSON()方法,能夠?qū)avaScript的Date對象自動轉(zhuǎn)換成ISO 8601日期字符串(與在Date對象上調(diào)用toISOString()的結(jié)果一樣)。
可以為任何對象添加toJSON()方法,如:
var book = {
"title": "Professional Javascript",
"authors": [
"Nicholas C. Zakas"
],
edition: 3,
year: 2011,
toJSON:function() {
return this.title;
}
};
var jsonText = JSON.stringify(book);
console.log(jsonText);
// "Professional Javascript"
以上代碼在book對象上定義了一個toJSON()對象,該方法放回圖書的書名。與Date對象類似,這個對象也將被序列化為一個簡單的字符串而非對象??梢宰宼oJSON()方法返回任何值,它都能正常工作。比如,可以讓這個方法返回undefined,此時如果包含它的對象嵌入在另一個對象中,會導(dǎo)致它的值變成null,而如果它是頂級對象,結(jié)果就是undefined。
toJSON()可以作為函數(shù)過濾器的補(bǔ)充,因此理解序列化的內(nèi)部順序十分重要。假設(shè)把一個對象傳入JSON.stringify(),序列化該對象的順序如下:
-
如果存在toJSON()方法而且能通過它取得有效的值,則調(diào)用該方法。否則返回對象本身。
-
如果提供了第二個參數(shù),應(yīng)用這個函數(shù)過濾器。傳入函數(shù)過濾器的值是第一步返回的值。
-
對第二步返回的值進(jìn)行相應(yīng)的序列化。
-
如果提供了第三個參數(shù),執(zhí)行相應(yīng)的格式化。
無論是考慮定義toJSON()的方法,還是考慮使用函數(shù)過濾器,亦或需要同時使用兩者,理解這個順序都是至關(guān)重要的。
解析選項
JSON.parse()方法也可以接收另一個參數(shù),該參數(shù)是一個函數(shù),將在每個鍵值對上調(diào)用。為了區(qū)別JSON.stringify()接收的替換(過濾)函數(shù)(replacer),這個函數(shù)被稱為還原函數(shù)(reviver),但實際上這兩個函數(shù)的簽名是相同的——它們都接收兩個參數(shù),一個鍵和一個值,而且都需要返回一個值。
如果還原函數(shù)返回undefined,則表示要從結(jié)果中刪除相應(yīng)的鍵;如果返回其他值,則將該值插入到結(jié)果中。在將日期字符串轉(zhuǎn)換為Date對象時,經(jīng)常要用到還原函數(shù)。例如:
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;
}
})
console.log(bookCopy.releaseDate.getFullYear());
以上代碼先是為book對象新增了一個releaseDate屬性,該屬性保存著一個Date對象。這個對象經(jīng)過序列化之后變成有效的JSON字符串,然后經(jīng)過解析又在bookCopy中還原為一個Date對象。還原函數(shù)在遇到releaseDate鍵時,會基于相應(yīng)的值創(chuàng)建一個新的Date對象。結(jié)果就是bookCopy.releaseDate屬性中會保存一個Date對象 。正因為如此,才能基于這個對象調(diào)用getFullYear()方法。