toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
20 stars 1 forks source link

详谈 JSON 与 JavaScript #228

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

JSON 在编程生涯简直就是无处不在啊。

一、JSON

JSON(JavaScript Object Natation)是一种轻量级的数据交换格式。由于易于阅读、编写,以及便于机器解析与生成的特性,相比 XML,它更小、更快、更易解析,使得它成为理想的数据交换语言。完全独立于语言的一种文本格式。

JSON 的两种结构:

  • “名称/值” 对的集合:不同语言中,它被理解成对象(object)、记录(record)、结构(struct)、字典(dictionary)、哈希表(hash table)、有键列表(keyed list)或者关联数组(associative array)。
  • 值的有序列表:大部分语言中,它被理解成数组(array)。

例如用以下 JSON 数据来描述一个人的信息:

{
  "name": "Frankie",
  "age": 20,
  "skills": ["Java", "JavaScript", "TypeScript"]
}

注意,JavaScript 不是 JSON,JSON 也不是 JavaScript。但 JSON 与 JavaScript 是存在渊源的,JSON 的数据格式是从 JavaScript 对象中演变出来的。(从名称上可以体现)

二、JSON 与 JavaScript 的区别

JSON 是一种数据格式,也可以说是一种规范。JSON 是用于跨平台数据交流的,独立于语言和平台。而 JavaScript 对象是一个实例,存在于内存中。JavaScript 对象是没办法传输的,只有在被序列化为 JSON 字符串后才能传输。

JavaScript 类型 JSON 的不同点
对象和数组 属性名称必须是双引号括起来的字符串;最后一个属性后不能有逗号
数值 禁止出现前导零( JSON.stringify() 方法自动忽略前导零,而在 JSON.parse() 方法中将会抛出 SyntaxError);如果有小数点,则后面至少跟着一位数字。
字符串 只有有限的一些字符可能会被转义;禁止某些控制字符; Unicode 行分隔符 (U+2028)和段分隔符 (U+2029)被允许 ; 字符串必须用双引号括起来。请参考下面的示例,可以看到 JSON.parse() 能够正常解析,但将其当作 JavaScript 解析时会抛出 SyntaxError 错误:
let code = '"\u2028\u2029"'
JSON.parse(code)  // 正常
eval(code)  // 错误

在 JavaScript 中,我们不能把以下对象叫做 JSON,如:

// 这只是 JS 对象
var people = {}

// 这跟 JSON 就更不沾边了,只是 JS 的对象
var people = { name: 'Frankie', age: 20 }

// 这跟 JSON 就更不沾边了,只是 JS 的对象
var people = { 'name': 'Frankie', 'age': 20 }

// 我们可以把这个称做:JSON 格式的 JS 对象
var people = { "name": "Frankie", "age": 20 }

// 我们可以把这个称做:JSON 格式的字符串
var people = '{"name":"Frankie","age":20}'

// 稍复杂的 JSON 格式的数组
var peopleArr = [
  { "name": "Frankie", "age": 20 },
  { "name": "Mandy", "age": 18 }
]

// 稍复杂的 JSON 格式字符串
var peopleStr = '[{"name":"Frankie","age":20},{"name":"Mandy","age":18}]'

尽管 JSON 与严格的 JavaScript 对象字面量表示方式很相似,如果将 JavaScript 对象属性加上双引号就理解成 JSON 是不对的,它只是符合 JSON 的语法规则而已。JSON 与 JavaScript 对象本质上是完全不同的两个东西,就像“斑马”和“斑马线”一样。

在 JavaScript 中,JSON 对象包含两个方法,用于解析的 JSON.parse() 和转换的 JSON.stringify() 方法。除了这两个方法,JSON 这个对象本身并没有其他作用,也不能被调用或者作为构造函数调用。

三、JSON.stringify()

将一个 JavaScript 对象或值转换为 JSON 字符串。

JSON.stringify(value, replacer, space)

const people = {
  name: 'Frankie',
  age: 20
}

const peopleStr1 = JSON.stringify(people, ['name'])
const peopleStr2 = JSON.stringify(people, (key, value) => {
  if (typeof value === 'string') {
    return undefined
  }
  return value
})

console.log(peopleStr1) // '{"name":"Frankie"}'
console.log(peopleStr2) // '{"age":20}'

一般来说,参数 replacerspace 平常比较少用到。

看示例:

const symbol = Symbol()

const func = () => { }

const people = {
  name: 'Frankie',
  age: 20,
  birthday: new Date(),
  sex: undefined,
  home: null,
  say: func,
  [symbol]: 'This is Symbol',
  skills: ['', undefined, , 'JavaScript', undefined, symbol, func],
  course: {
    name: 'English',
    score: 90
  },
  prop1: NaN,
  prop2: Infinity,
  prop3: new Boolean(true) // or new String('abc') or new Number(10)
}

const replacer = (key, value) => {
  // 这里我其实没做什么处理,跟忽略 replacer 参数是一致的。

  // 若符合某种条件不被序列化,return undefined 即可。
  // 比如 if (typeof value === 'string') return undefined

  // 也可以通过该函数来看看序列化的执行顺序。

  // console.log('key: ', key)
  // console.log('value: ', value)
  return value
}

// 序列化操作
const peopleStr = JSON.stringify(people, replacer)

// '{"name":"Frankie","age":20,"birthday":"2021-01-17T10:24:39.333Z","home":null,"skills":["",null,null,"JavaScript",null,null,null],"course":{"name":"English","score":90},"prop1":null,"prop2":null,"prop3":true}'
console.log(peopleStr) 

console.log(JSON.stringify(function(){})) // undefined
console.log(JSON.stringify(undefined)) // undefined
结合以上示例,有以下特点:
针对最后两点举例说明:

console.log(JSON.stringify(people)) // 结果是 {"name":"Mandy"},而不是 {"name":"Frankie","age":20} // 需要注意的是,若对象的 toJSON 属性值不是函数的话,仍然是该对象作为参数进行序列化。

// 上面还提到 Date 对象本身内置了 toJSON() 方法,所以以下返回结果是: // "2021-01-17T09:40:08.302Z" console.log(JSON.stringify(new Date()))

// 假如我去修改 Date 原型上的 toJSON 方法,结果会怎样呢? Date.prototype.toJSON = function () { return '被改写了' } console.log(JSON.stringify(new Date())) // "被改写了"


* 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
```js
const foo = {}
const bar = {
  b: foo
}
foo.a = bar
console.log(foo)

// 如果这时候对 foo 进行序列化操作,就会抛出错误。
JSON.stringify(foo) // Uncaught TypeError: Converting circular structure to JSON

foo 对象和 bar 对象会无限相互引用,可以看下 foo 打印结果如下,如果此时对 foo 进行序列化操作,就会抛出错误:Uncaught TypeError: Converting circular structure to JSON

针对这个问题,看看别人的解决方法,看这里 JSON-js。具体用法是,先引入其中的 cycle.js 脚本,然后 JSON.stringify(JSON.decycle(foo)) 就 OK 了。

JSON.stringify() 总结:
  1. 若被序列化的对象,存在 toJSON() 方法,真正被序列化的其实是 toJSON() 方法的返回值。
  2. 若提供了 replacer 参数,应用这个函数过滤器,传入的函数过滤器的值是第 1 步返回的值。
  3. 对第 2 步返回的每个值,进行相应的序列化。
  4. 如果提供了 space 参数,执行相应的格式化操作。

四、JSON.parse()

JSON.parse() 方法用来解析 JSON 字符串,构造由字符串描述的 JavaScript 值或对象。提供可选的 reviver 函数用以在返回之前对所得到的对象执行变换(操作)。

JSON.parse(text, reviver)

// JSON 字符串
const peopleStr = '{"name":"Frankie","age":20,"birthday":"2021-01-17T10:24:39.333Z","home":null,"skills":["",null,null,"JavaScript",null,null,null],"course":{"name":"English","score":90},"prop1":null,"prop2":null,"prop3":true}'

// 若需输出 this 对象,不能使用箭头函数
const reviver = (key, value) => {
  // 可以通过该函数来看看序列化的执行顺序。
  // console.log('key: ', key)
  // console.log('value: ', value)

  // 若删除某个属性,return undefined 即可。
  // 比如 if (typeof value === 'string') return undefined

  // 如果到了最顶层,则直接返回属性值
  if (key === '') return value

  // 数值类型,将属性值变为原来的 2 倍
  if (typeof value === 'number') return value * 2

  // 其他的原样解析
  return value
}

const parseObj = JSON.parse(peopleStr, reviver)

console.log(parseObj)

解析结果如下:

{
  "age": 40,
  "birthday": "2021-02-14T06:02:18.491Z",
  "course": { "name": "English", "score": 180 },
  "home": null,
  "name": "Frankie",
  "prop1": null,
  "prop2": null,
  "prop3": true,
  "skills": ["", null, null, "JavaScript", null, null, null]
}

五、其他

针对 Line separator 和 Paragraph separator 的处理,可以看这里:JSON.stringify 用作 JavaScript

六、拓展

根据 ECMA-262 标准定义,一个字符串可以包含任何东西,只要它不是一个引号,一个反斜线或者一个行终止符。

以下被认为是行终止符:

七、参考