实现 JSON.stringify 与 JSON.parse
定义
MDN
JSON 对象包含两个方法:用于解析 JavaScript Object Notation (JSON) 的
parse()
方法,以及将对象/值转换为 JSON 字符串的stringify()
方法。除了这两个方法,JSON 这个对象本身并没有其他作用,也不能被调用或者作为构造函数调用。JSON 是一种语法,用来序列化对象、数组、数值、字符串、布尔值和
null
。它基于 JavaScript 语法,但与之不同:JavaScript 不是 JSON,JSON 也不是 JavaScript。
JavaScript 与 JSON 的差异
JSON 是用于序列化对象、数组、数字、字符串、布尔值和 null 的语法。它基于 JavaScript 语法,但与 JavaScript 不同:大多数 JavaScript 都不是 JSON。例如:
- Objects and Arrays 对象和数组。
- 属性名称必须是双引号字符串;禁止尾随逗号。
- 数字
- 禁止使用前导零。
- 小数点后必须至少跟一位数字。
- 不支持 NaN 和 Infinity。
任何 JSON 文本都是有效的 JavaScript 表达式,但仅在 JSON 超集修订之后。在修订之前,允许在 JSON 中的字符串文本和属性键中使用 U+2028 行分隔符和 U+2029 段落分隔符;但在 JavaScript 字符串文字中相同的用法是 SyntaxError。
其他差异包括只允许双引号字符串,不支持 undefined 或注释。对于那些希望使用基于 JSON 的更人性化的配置格式的人来说,有 Babel 编译器使用的 JSON5 和更常用的 YAML。
JSON.stringify(value[, replacer [, space]])
JSON.stringify
是浏览器高版本带的一个将 JS 的 Object 对象转换为JSON 字符串的一个方法
该方法将一个 JavaScript
对象或值转换为 JSON
字符串,如果指定了一个 replacer
函数,则可以选择性地替换值,或者指定的 replacer
是数组,则可选择性地仅包含数组指定的属性。
- 异常
- 当在循环引用时会抛出异常
TypeError
("cyclic object value")(循环对象值)obj['a'] = obj - 当尝试去转换
BigInt
类型的值会抛出TypeError
("BigInt value can't be serialized in JSON")(BigInt
值不能JSON
序列化)、
- 当在循环引用时会抛出异常
- 处理规则
- 转换值如果有
toJSON()
方法,该方法定义什么值将被序列化。 - 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
- 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
undefined
、任意的函数以及symbol
值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成null
(出现在数组中时)。函数、undefined
被单独转换时,会返回undefined
,如JSON.stringify(function(){})
orJSON.stringify(undefined)
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
- 所有以
symbol
为属性键的属性都会被完全忽略掉,即便replacer
参数中强制指定包含了它们。 Date
日期调用了toJSON()
将其转换为了string
字符串(同Date.toISOString()
),因此会被当做字符串处理 (格式如2022-02-17T05:46:30.441Z
)。NaN
和Infinity
,-Infinity
格式的数值及null
都会被当做null
。- 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性(不可枚举的属性默认会被忽略)。
- 转换值如果有
简单记忆
- 会处理的几种类型:String, Number, Boolean, null, Array, Object
- 不会处理的几种类型:Date, RegExp, Symbol, undefined, Function
const noop = () => {}
const obj = {
null: null,
undefined: undefined, // 忽略,在数组中会转为 null
symbol: Symbol(), // 忽略,在数组中会转为 null
fn_1: noop, // 忽略,在数组中会转为 null
nan: NaN, // 转为 null
regexp: /abc/, // 转为 '{}' 字符串
num_1: 0,
num_2: -0, // 转为 0
num_3: Infinity, // 转为 null
num_4: -Infinity, // 转为 null
date: new Date('2022-02-17T08:53:59.659Z'), // 转为字符串 2022-02-17T08:53:59.659Z
bool_1: true,
bool_2: false,
// 值在数组中
arr_1: [NaN, 0, -0, Infinity, new Date('2022-02-17T08:53:59.659Z')],
arr_2: [undefined, Symbol(), noop] // 作为属性会忽略,在数组中会转为 null
}
// key 为 Symbol 也会忽略
console.log(JSON.stringify(noop)) // 函数 单独转换时,会返回 undefined
console.log(JSON.stringify(undefined)) // undefined 单独转换时,会返回 undefined
console.log(JSON.stringify(obj, null, 2))
参见手写实现 JSON.stringify,加深理解
手写实现 JSON.stringify
// 实现 JSON.stringify
// 参见 https://github.com/YvetteLau/Step-By-Step/issues/39#issuecomment-508327280
const jsonStringify = function jsonStringify(data) {
let dataType = typeof data
if (dataType !== 'object') {
let result = data
// data 可能是 string/number/null/undefined/boolean
if (Number.isNaN(data) || data === Infinity) {
// NaN 和 Infinity 序列化返回 'null'
result = 'null'
} else if (
dataType === 'function' ||
dataType === 'undefined' ||
dataType === 'symbol'
) {
// function, undefined, symbol 序列化返回 undefined
return undefined
} else if (dataType === 'string') {
result = `"${data}"`
}
// boolean 返回 String()
return String(result)
} else if (dataType === 'object') {
if (data === null) {
return 'null'
} else if (data.toJSON && typeof data.toJSON === 'function') {
return jsonStringify(data.toJSON())
} else if (data instanceof Array) {
let result = []
// 如果是数组
// toJSON 方法可以存在于原型链中
data.forEach((item, index) => {
if (
typeof item === 'undefined' ||
typeof item === 'function' ||
typeof item === 'symbol'
) {
result[index] = 'null'
} else {
result[index] = jsonStringify(item)
}
})
result = `[${result}]`
return result.replace(/'/g, `"`)
} else {
// 普通对象
/**
* 循环引用抛错 (暂未检测,循环引用时,堆栈溢出)
* symbol key 忽略
* undefined, 函数,symbol 为属性值,被忽略
*/
let result = []
Object.keys(data).forEach((item, index) => {
if (typeof item !== 'symbol') {
// key 如果是 symbol 对象,忽略
if (
data[item] !== undefined &&
typeof data[item] !== 'function' &&
typeof data[item] !== 'symbol'
) {
// 键值如果是 undefined、函数、symbol 为属性值,忽略
result.push(`"${item}":${jsonStringify(data[item])}`)
}
}
})
return `{${result}}`.replace(/'/g, `"`)
}
}
}
JSON.parse(text[, reviver])
该方法用来解析JSON
字符串,构造由字符串描述的JavaScript
值或对象。提供可选的 reviver
函数用以在返回之前对所得到的对象执行变换 (操作)。
如果指定了 reviver
函数,则解析出的 JavaScript
值(解析值)会经过一次转换后才将被最终返回(返回值)。
- 异常
- 若传入的字符串不符合
JSON
规范,则会抛出SyntaxError
异常。 JSON.parse()
不允许用逗号作为结尾
- 若传入的字符串不符合
const jsonStr = '{}'
console.log(JSON.parse(jsonStr))
手写实现 JSON.parse
- 使用 eval 实现
- 使用 new Function 实现
手写实现 JSON.parse
// 实现 JSON.parse
// 1. 使用 eval 实现
function parse1(json) {
return eval(`(${json})`)
}
// 2. 上述方案如果数据中传入了可执行的 JS 代码,很可能造成 XSS 攻击,
// 因此调用 eval 之前,需要对数据进行校验
function parse2(json) {
const rx_one = /^[\],:{}\s]*$/
const rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g
const rx_three =
/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g
const rx_four = /(?:^|:|,)(?:\s*\[)+/g
if (
rx_one.test(
json.replace(rx_two, '@').replace(rx_three, ']').replace(rx_four, '')
)
) {
return eval(`(${json})`)
}
}
// 3. 使用 new Function 实现
// Function 与 eval 有相同的字符串参数特性
function parse3(json) {
return new Function(`return ${json}`)()
}
// 以下是 MDN polyfill 的实现
// From https://github.com/douglascrockford/JSON-js/blob/master/json2.js
if (typeof JSON.parse !== 'function') {
var rx_one = /^[\],:{}\s]*$/
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g
var rx_three =
/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g
var rx_four = /(?:^|:|,)(?:\s*\[)+/g
var rx_dangerous =
/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k
var v
var value = holder[key]
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k)
if (v !== undefined) {
value[k] = v
} else {
delete value[k]
}
}
}
}
return reviver.call(holder, key, value)
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text)
rx_dangerous.lastIndex = 0
if (rx_dangerous.test(text)) {
text = text.replace(rx_dangerous, function (a) {
return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
})
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with "()" and "new"
// because they can cause invocation, and "=" because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
// replace all simple value tokens with "]" characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or "]" or
// "," or ":" or "{" or "}". If that is so, then the text is safe for eval.
if (
rx_one.test(
text
.replace(rx_two, '@')
.replace(rx_three, ']')
.replace(rx_four, '')
)
) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The "{" operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')')
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function'
? walk(
{
'': j
},
''
)
: j
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse')
}
}
扩展
资料
- MDN JSON
- MDN
JSON.stringify
- MDN
JSON.parse
参考
- 深拷贝系列 ———— 自己实现一个 JSON.stringify 和 JSON.parse
- 实现一个 JSON.stringify
- JSON 之父 Douglas Crockford 写的 ployfill,里面提供了三种实现方式
- 推荐:上文解析 JSON.parse 三种实现方式
- 关于模板引擎的工作方式和性能?