[TEAP早期試讀]HTML5與JSON

李鬆峰發表於2012-03-13

圖靈社群按

TEAP是什麼?TEAP是Turingbook Early Access Program的簡稱,即早期試讀,它公佈的是圖靈在途新書未經編輯的內容。一本書的翻譯週期約為3到6個月,如果在翻譯過程中,譯者就能與讀者進行溝通和交流,對整本書的翻譯品質是有幫助的。通過TEAP,讀者可以提前閱讀將來才能出版的內容,譯者也能收穫寶貴的反饋意見,改進翻譯,提高質量。

本章是《JavaScript高階程式設計(第3版)》的第20章。

本章內容


  • 理解JSON語法
  • 解析JSON
  • 序列化JSON

曾經有一段時間,XML是網際網路上傳輸結構化資料的事實標準。Web服務的第一次浪潮很大程度上都是建立在XML之上的,突出的特點是伺服器與伺服器間通訊。然而,業界一直不乏質疑XML的聲音。不少人認為XML過於煩瑣、冗長。為解決這個問題,也湧現了一些方案。不過,Web的發展方向已經改變了。

2006年,Douglas Crockford把JSON(JavaScript Object Notation,JavaScript物件表示法)作為IETF RFC 4627提交給IETF,而JSON的應用早在2001年就已經開始了。JSON是JavaScript的一個嚴格的子集,利用了JavaScript中的一些模式來表示結構化資料。Crockford認為與XML相比,JSON是在JavaScript中讀寫結構化資料的更好的方式。因為可以把JSON直接傳給eval(),而且不必建立DOM物件。

關於JSON,最重要的是要理解它是一種資料格式,不是一種程式語言。雖然具有相同的語法形式,但JSON並不從屬於JavaScript。而且,並不是只有JavaScript才使用JSON,畢竟JSON只是一種資料格式。很多程式語言都有針對JSON的解析器和序列化器。

20.1 語法

JSON的語法可以表示以下三種型別的值。

  • 簡單值:使用與JavaScript相同的語法,可以在JSON中表示字串、數值、布林值和null。但JSON不支援JavaScript中的特殊值undefined。
  • 物件:物件作為一種複雜資料型別,表示的是一組有序的鍵值對兒。而每個鍵值對兒中的值可以是簡單值,也可以是複雜資料型別的值。
  • 陣列:陣列也是一種複雜資料型別,表示一組有序的值的列表,可以通過數值索引來訪問其中的值。陣列的值也可以是任意型別——簡單值、物件或陣列。 JSON不支援變數、函式或物件例項,它就是一種表示結構化資料的格式,雖然與JavaScript中表示資料的某些語法相同,但它並不侷限於JavaScript的範疇。

20.1.1 簡單值

最簡單的JSON資料形式就是簡單值。例如,下面這個值是有效的JSON資料:

5

這是JSON表示數值5的方式。類似地,下面是JSON表示字串的方式:

"Hello world!"

JavaScript字串與JSON字串的最大區別在於,JSON字串必須使用雙引號(單引號會導致語法錯誤)。

布林值和null也是有效的JSON形式。但是,在實際應用中,JSON更多地用來表示更復雜的資料結構,而簡單值只是整個資料結構中的一部分。

20.1.2 物件

JSON中的物件與JavaScript字面量稍微有一些不同。下面是一個JavaScript中的物件字面量:

var person = { 
    name: "Nicholas",
    age: 29
};

這雖然是開發人員在JavaScript中建立物件字面量的標準方式,但JSON中的物件要求給屬性加引號。實際上,在JavaScript中,前面的物件字面量完全可以寫成下面這樣:

var object = { 
    "name": "Nicholas",
    "age": 29
};

JSON表示上述物件的方式如下:

{ 
    "name": "Nicholas",
    "age": 29
}

與JavaScript的物件字面量相比,JSON物件有兩個地方不一樣。首先,沒有宣告變數(JSON中沒有變數的概念)。其次,沒有末尾的分號(因為這不是JavaScript語句,所以不需要分號)。再說一遍,物件的屬性必須加雙引號,這在JSON中是必需的。屬性的值可以是簡單值,也可以是複雜型別值,因此可以像下面這樣在物件中嵌入物件:

{ 
    "name": "Nicholas",
    "age": 29,
    "school": {
        "name": "Merrimack College",
        "location": "North Andover, MA"
    }
}

這個例子在頂級物件中嵌入了學校("school")資訊。雖然有兩個"name"屬性,但由於它們分別屬於不同的物件,因此這樣完全沒有問題。不過,同一個物件中絕對不應該出現兩個同名屬性。

與JavaScript不同,JSON中物件的屬性名任何時候都必須加雙引號。手工編寫JSON時,忘了給物件屬性名加雙引號或者把雙引號寫成單引號都是常見的錯誤。

20.1.3 陣列

JSON中的第二種複雜資料型別是陣列。JSON陣列採用的就是JavaScript中的陣列字面量形式。例如,下面是JavaScript中的陣列字面量:

var values = [25, "hi", true];

在JSON中,可以採用同樣的語法表示同一個陣列:

[25, "hi", true]

同樣要注意,JSON陣列也沒有變數和分號。把陣列和物件結合起來,可以構成更復雜的資料集合,例如:

[
    {
         "title": "Professional JavaScript",
         "authors": [
             "Nicholas C. Zakas"
         ],
         edition: 3,
         year: 2011
    },
    {
         "title": "Professional JavaScript",
         "authors": [
         "Nicholas C. Zakas"
         ],
         edition: 2,
         year: 2009
    },
    {
         "title": "Professional Ajax",
         "authors": [
             "Nicholas C. Zakas",
             "Jeremy McPeak",
             "Joe Fawcett"
         ],
         edition: 2,
         year: 2008
    },
    {
         "title": "Professional Ajax",
         "authors": [
             "Nicholas C. Zakas",
             "Jeremy McPeak",
             "Joe Fawcett"
         ],
         edition: 1,
         year: 2007
    },
    {
         "title": "Professional JavaScript",
         "authors": [
         "Nicholas C. Zakas"
         ],
         edition: 1,
             year: 2006
    }
]

這個陣列中包含一些表示圖書的物件。每個物件都有幾個屬性,其中一個屬性是"authors",這個屬性的值又是一個陣列。物件和陣列通常是JSON資料結構的最外層形式(當然,這不是強制規定的),利用它們能夠創造出各種各樣的資料結構。

20.2 解析與序列化

JSON之所以流行,擁有與JavaScript類似的語法並不是全部原因。更重要的一個原因是,可以把JSON資料結構解析為有用的JavaScript物件。與XML資料結構要解析成DOM文件而且從中提取資料極為麻煩相比,JSON可以解析為JavaScript物件的優勢極其明顯。就以上一節中包含一組圖書的JSON資料結構為例,在解析為JavaScript物件後,只需要下面一行簡單的程式碼就可以取得第三本書的書名:

books[2].title

當然,這裡是假設把解析後JSON資料結構得到的物件儲存到了變數books中。再看看下面在DOM結構中查詢資料的程式碼:

doc.getElementsByTagName("book")[2].getAttribute("title")

看看這些多餘的方法呼叫,就不難理解為什麼JSON能得到JavaScript開發人員的熱烈歡迎了。從此以後,JSON就成了Web服務開發中交換資料的事實標準。

20.2.1 JSON物件

早期的JSON解析器基本上就是使用JavaScript的eval()函式。由於JSON是JavaScript語法的子集,因此eval()函式可以解析、解釋並返回JavaScript物件和陣列。ECMAScript 5對解析JSON的行為進行規範,定義了全域性物件JSON。支援這個物件的瀏覽器有IE8+、Firefox 3.5+、Safari 4+、Chrome和Opera 10.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,該方法會丟擲錯誤。

20.2.2 序列化選項

實際上,JSON.stringify()除了要序列化的JavaScript物件外,還可以接收另外兩個引數,這兩個引數用於指定以不同的方式序列化JavaScript物件。第一個引數是個過濾器,可以是一個陣列,也可以是一個函式;第二個引數是一個選項,表示是否在JSON字串中保留縮排。單獨或組合使用這兩個引數,可以更全面深入地控制JSON的序列化。

1. 過濾結果

如果過濾器引數是陣列,那麼JSON.stringify()的結果中將只包含陣列中列出的屬性。來看下面的例子。

var book = { 
               "itle": "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": [
                    "icholas 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 undefi ned;

        default:
            return value;
    }
});

這裡,函式過濾器根據傳入的鍵來決定結果。如果鍵為"authors",就將陣列連線為一個字串;如果鍵為"year",則將其值設定為5000;如果鍵為"edition",通過返回undefined刪除該屬性。最後,一定要提供default項,此時返回傳入的值,以便其他值都能正常出現在結果中。實際上,第一次呼叫這個函式過濾器,傳入的鍵是一個空字串,而值就是book物件。序列化後的JSON字串如下所示:

{"title":"Professional JavaScript","authors":"Nicholas C. Zakas","year":5000}

要序列化的物件中的每一個物件都要經過過濾器,因此陣列中的每個帶有這些屬性的物件經過過濾之後,每個物件都只會包含"title"、"authors"和"year"屬性。

Firefox 3.5和3.6對JSON.stringify()的實現有一個bug,在將函式作為該方法的第二個引數時這個bug就會出現,即這個函式只能作為過濾器:返回undefined意味著要跳過某個屬性,而返回其他任何值都會在結果中包含相應的屬性。Firefox 4修復了這個bug。

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
}

不知道讀者注意到沒有,JSON.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物件自動轉換成ISO 8601日期字串(與在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(),序列化該物件的順序如下。

  1. 如果存在toJSON()方法而且能通過它取得有效的值,則呼叫該方法。否則,按預設順序執行序列化。
  2. 如果提供了第二個引數,應用這個函式過濾器。傳入函式過濾器的值是第(1)步返回的值。
  3. 對第2步返回的每個值進行相應的序列化。
  4. 如果提供了第三個引數,執行相應的格式化。

無論是考慮定義toJSON()方法,還是考慮使用函式過濾器,亦或需要同時使用兩者,理解這個順序都是至關重要的。

20.2.3 解析選項

JSON.parse()方法也可以接收另一個引數,該引數是一個函式,將在每個鍵值對兒上呼叫。為了區別JSON.stringify()接收的替換(過濾)函式(replacer),這個函式被稱為還原函式(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()方法。

20.3 小結

JSON是一個輕量級的資料格式,可以簡化表示複雜資料結構的工作量。JSON使用JavaScript語法的子集表示物件、陣列、字串、數值、布林值和null。即使XML也能表示同樣複雜的資料結果,但JSON沒有那麼煩瑣,而且在JavaScript中使用更便利。

ECMAScript 5定義了一個原生的JSON物件,可以用來將物件序列化為JSON字串或者將JSON資料解析為JavaScript物件。JSON.stringify()和JSON.parse()方法分別用來實現上述兩項功能。這兩個方法都有一些選項,通過它們可以改變過濾的方式,或者改變序列化的過程。

原生的JSON物件也得到了很多瀏覽器的支援,比如IE8+、Firefox 3.5+、Safari 4+、Opera 10.5和Chrome。

[完]

相關文章