1. 简介

曾经有一段时间,XML 是互联网上传输结构化数据的事实标准。Web 服务的第一次浪潮很大程度上都是建立在 XML之上的,突出特点是服务器与服务器之间的通信。但是 XML 过于繁琐、冗长。

JSON 格式(JavaScript Object Notation 的缩写,JavaScript 对象表达式)是一种用于数据交换的文本格式,2001年由 Douglas Crockford 提出,目的是取代繁琐笨重的 XML 格式。

关于 JSON,最重要的是要理解它是一种数据格式,而不是一种编程语言。另外,JSON 并不从属于 JavaScript,且并不是 JavaScript 才可以使用 JSON,很多语言都有针对 JSON 的解析器和序列化器。

相比 XML 格式,JSON 格式有两个显著的优点:

  • 书写简单,一目了然;

  • 符合 JavaScript 原生语法,可以由解释引擎直接处理,不用另外添加解析代码。

所以,JSON 迅速被接受,已经成为各大网站交换数据的标准格式,并被写入 ECMAScript 5,成为标准的一部分。

2. 基本语法

简单说,每个 JSON 对象,就是一个值,且每个 JSON 文档只能包含一个值。

  • 简单类型的值

  • 对象

  • 数组

JSON 对值的类型和格式有严格的规定。

  • 简单类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和null(不能使用 NaN, Infinity, -Infinityundefined)。

  • 字符串必须使用双引号表示,不能使用单引号。

  • 对象的键名必须放在双引号里面。

  • 数组或对象最后一个成员的后面,不能加逗号。

以下是合格的 JSON 值。

  1. ["one", "two", "three"]
  2. { "one": 1, "two": 2, "three": 3 }
  3. {"names": ["张三", "李四"] }
  4. [ { "name": "张三"}, {"name": "李四"} ]

以下是不合格的 JSON 值。

  1. { name: "张三", 'age': 32 } // 属性名必须使用双引号
  2. [32, 64, 128, 0xFFF] // 不能使用十六进制值
  3. { "name": "张三", "age": undefined } // 不能使用undefined
  4. { "name": "张三",
  5. "birthday": new Date('Fri, 26 Aug 2011 07:13:10 GMT'),
  6. "getName": function() {
  7. return this.name;
  8. }
  9. } // 不能使用函数和日期对象

需要注意的是,空数组和空对象都是合格的 JSON 值,null 本身也是一个合格的 JSON 值。

ES5 新增了 JSON 对象,用来处理 JSON 格式数据。它有两个方法:JSON.stringify()JSON.parse()

3. JSON 的解析和序列化

JSON 之所以流行,拥有与 JavaScript 类似的语法并不是全部原因。更重要的原因是,可以把 JSON 数据结构解析为有用的 JavaScript 对象。

与 XML 数据结构要解析为 DOM 文档而且从中提取数据极为麻烦相比,JSON 可以解析为 JavaScript 对象的优势及其明显。

早期的 JSON 解析器基本上就是使用 JavaScript 的 eval 函数,eval 函数可以解析、解释并返回 JavaScript 对象和数组。而使用 eval 求值存在风险,因为可能包含一些恶意代码。

后来,ES5 对解析 JSON 的行为进行了规范,定义了一个全局对象 JSON。这个对象包含两个方法:stringfy 和 parse。分别用于把 JavaScript 对象序列化为 JSON 字符串和把 JSON 字符串解析为原生 JavaScript 值。

4. JSON.stringfy 序列化

4.1 基本语法

JSON.stringify 方法用于将一个值转为符合 JSON 格式的字符串,默认情况下,输出的 JSON 字符串不包含任何空格字符或缩进。

  1. JSON.stringify('abc') // ""abc""
  2. JSON.stringify(1) // "1"
  3. JSON.stringify(false) // "false"
  4. JSON.stringify([]) // "[]"
  5. JSON.stringify({}) // "{}"
  6. JSON.stringify([1, "false", false])
  7. // "[1,"false",false]"
  8. JSON.stringify({ name: "张三" })
  9. // "{"name":"张三"}"

需要注意的是,对于原始类型的字符串,转换结果会带双引号。

  1. JSON.stringify('foo') === "foo" // false
  2. JSON.stringify('foo') === "\"foo\"" // true

上面代码中,字符串 foo,被转成了 "foo"。这是因为将来还原的时候,双引号可以让 JavaScript 引擎知道,foo 是一个字符串,而不是一个变量名。

如果原始对象中,有一个成员的值是 undefined、函数或 XML 对象,这个成员会被过滤。

  1. var obj = {
  2. a: undefined,
  3. b: function () {}
  4. };
  5. JSON.stringify(obj) //=> "{}"

如果数组的成员是 undefined、函数或 XML 对象,则这些值被转成 null

  1. var arr = [undefined, function () {}];
  2. JSON.stringify(arr) // "[null,null]"

正则对象会被转成空对象。

  1. JSON.stringify(/foo/) // "{}"
  2. var obj = {
  3. a: undefined,
  4. b: function () {},
  5. c: /ab/
  6. };
  7. JSON.stringify(obj) //=> "{"c":{}}"

JSON.stringify 方法会忽略对象的不可遍历属性。

var obj = {};
Object.defineProperties(obj, {
  'foo': {
    value: 1,
    enumerable: true
  },
  'bar': {
    value: 2,
    enumerable: false
  }
});

JSON.stringify(obj); // "{"foo":1}"

4.2 过滤器参数

JSON.stringify 方法接受的第二个参数是一个过滤器

(1)可以是一个数组,指定需要转成字符串的属性。

var obj = {
  'prop1': 'value1',
  'prop2': 'value2',
  'prop3': 'value3'
};

var selectedProperties = ['prop1', 'prop2'];

JSON.stringify(obj, selectedProperties)
// "{"prop1":"value1","prop2":"value2"}"
//=> 只转 prop1 和 prop2 两个属性。

这个类似“白名单”的数组,只对对象的属性有效,对数组无效。

JSON.stringify(['a', 'b'], ['0'])
// "["a","b"]"

JSON.stringify({0: 'a', 1: 'b'}, ['0'])
// "{"0":"a"}"

(2)可以是一个函数,用来更改 JSON.stringify 的默认行为,同样只对对象有效。

function f(key, value) {
  if (typeof value === "number") {
    value = 2 * value;
  }
  return value;
}

JSON.stringify({ a: 1, b: 2 }, f)
// '{"a": 2,"b": 4}'

注意,这个处理函数是递归处理所有的键

var o = {a: {b: 1}, c: 2};

function f(key, value) {
  console.log("["+ key +"]:" + value);
  return value;
}

JSON.stringify(o, f)
// []:[object Object]
// [a]:[object Object]
// [b]:1
// [c]:2

上面代码中,对象 o 一共会被 f 函数处理四次。第一次键名为空,键值是整个对象 o;第二次键名为 a,键值是 {b: 1};第三次键名为 b,键值为 1,第四次键名为 c,键值为 2

递归处理中,每一次处理的对象,都是前一次返回的值。

var o = {a: 1};

function f(key, value) {
  if (typeof value === 'object') {
    return {b: 2};
  }
  return value * 2;
}

JSON.stringify(o,f)
// "{"b": 4}"

上面代码中,f 函数修改了对象 o,接着 JSON.stringify 方法就递归处理修改后的对象 o。

如果处理函数返回 undefined 或没有返回值,则该属性会被忽略。

function f(key, value) {
  if (typeof(value) === "string") {
    return undefined;
  }
  return value;
}

JSON.stringify({ a: "abc", b: 123 }, f)
// '{"b": 123}'

上面代码中,a 属性经过处理后,返回 undefined,于是该属性被忽略了。

4.3 缩进参数

JSON.stringify 还可以接受第三个参数,用于控制结果汇总的缩进和空白符,增强返回的 JSON 字符串的可读性。

  • 如果是数字,表示每个级别的缩进空格数(最多不超过 10 个,大于 10 的值都会被自动变成 10)

  • 如果是字符串(不超过 10 个字符,超过则取前面 10 个),用作缩进字符

JSON.stringify({ p1: 1, p2: 2 }, null, 2);
/*
"{
  "p1": 1,
  "p2": 2
}"
*/

JSON.stringify({ p1:1, p2:2 }, null, '|-');
/*
"{
|-"p1": 1,
|-"p2": 2
}"
*/

需要注意的是:只要传入了有效控制缩进的参数值,结果字符串就会包含换行符,因为只缩进不换行没有意义,所以其内部实现了。

4.4 toJSON 方法

可以为任何对象添加 toJSON 方法。

如果对象有自定义的 toJSON 方法,那么 JSON.stringify 会使用这个方法的返回值作为参数,而忽略原对象的其他属性。

下面是一个普通的对象。

var user = {
  firstName: '三',
  lastName: '张',

  get fullName(){
    return this.lastName + this.firstName;
  }
};

JSON.stringify(user)
// "{"firstName":"三","lastName":"张","fullName":"张三"}"

现在,为这个对象加上 toJSON 方法。

var user = {
  firstName: '三',
  lastName: '张',

  get fullName(){
    return this.lastName + this.firstName;
  },

  toJSON: function () {
    var data = {
      firstName: this.firstName,
      lastName: this.lastName
    };
    return data;
  }
};

JSON.stringify(user)
// "{"firstName":"三","lastName":"张"}"

更简单的例子:

var book = {
  title: 'aa',
  author: 'bb',
  toJSON: function() {
    return this.title;
  }
}
JSON.stringify(book); //=> ""aa""
//=> 需要注意的是:在内部这个 toJOSN 是通过这个 book 调用的,所以 this 还是指向 book

Date 对象就有一个自己的 toJSON 方法,与在 Date 对象上调用 toISOString 方法结果完全一样

var date = new Date('2015-01-01');
date.toJSON() // "2015-01-01T00:00:00.000Z"
JSON.stringify(date) // ""2015-01-01T00:00:00.000Z""

toJSON 方法的一个应用是,将正则对象自动转为字符串。因为 JSON.stringify 默认不能转换正则对象,但是设置了 toJSON 方法以后,就可以转换正则对象了。

var obj = {
  reg: /foo/
};

// 不设置 toJSON 方法时
JSON.stringify(obj) // "{"reg":{}}"

// 设置 toJSON 方法时
RegExp.prototype.toJSON = RegExp.prototype.toString;
JSON.stringify(/foo/); //=> ""/foo/""

在正则对象的原型上面部署了 toJSON 方法,将其指向 toString 方法,因此遇到转换成 JSON 时,正则对象就先调用 toJSON 方法转为字符串,然后再被 JSON.stingify 方法处理。

4.5 序列化对象的顺序

(1)对象中管是否存在 toJSON 方法,如果存在,则调用该对象的 toJSON 方法,使用其返回值作为 JSON.stringify 的返回值,忽略其他参数

(2)没有 toJSON 方法,如果存在第二个参数,应用这个过滤器

(3)对过滤器返回的每个值进行相应的序列化

(4)如果提供了第三个参数,执行相应的格式化

5. JSON.parse 解析

JSON.parse 方法用于将 JSON 字符串转化成对象。

JSON.parse('{}') // {}
JSON.parse('true') // true
JSON.parse('"foo"') // "foo"
JSON.parse('[1, 5, "false"]') // [1, 5, "false"]
JSON.parse('null') // null

var o = JSON.parse('{"name": "张三"}');
o.name // 张三

如果传入的字符串不是有效的 JSON 格式,JSON.parse 方法将报错。

JSON.parse("'String'") // illegal single quotes
// SyntaxError: Unexpected token ILLEGAL

为了处理解析错误,可以将 JSON.parse 方法放在 try...catch 代码块中。

5.1 还原函数

JSON.parse 方法可以接受一个还原函数,与 JSON.stringify 方法传入的过滤函数相对,用法类似。

function f(key, value) {
  if (key === ''){
    return value;
  }
  if (key === 'a') {
    return value + 10;
  }
}

var o = JSON.parse('{"a":1,"b":2}', f);
o.a // 11
o.b // undefined

在日期字符串转换为 Date 对象时,经常要用到还原函数

var book = {
  title: 'aa',
  author: 'bb',
  publishDate: newDate(2018,1,1)
}

var jsonText = JSON.stringify(book);

var bookCopy = JSON.parse(jsonText, function(key, value) {
  if (key === 'publishDate') {
    return new Date(value)
  }
  return value;
});