这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战
前言
JSON是日常开发中经常用到的一个对象,它包含两个方法:stringify用于将对象转换成JSON字符串,即序列化;parse()用于将JSON字符串转换成JS对象,即反序列化。这次我们来手动实现下stringify这个方法,通过实现的过程,我们能够站在全局考察自己对 JS 各种数据类型理解的深度,对各种极端的边界情况处理能力,以及编码能力。
方法介绍
stringify方法的作用是将一个JS对象或值转换成JSON格式的字符串,它有三个参数:
JSON.stringify(value[, replacer [, space]])
value:要进行序列化转换的一个JS对象或值。replacer [Function | Array]:可选参数。如果是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的JSON字符串中;如果该参数为null或者无,则对象所有的属性都会被序列化。space [Number | String]:可选参数。指定缩进用的空白字符串,用于格式化转换的结果字符串;如果参数是个数字,则代表有缩进有多少个空格;上限为10。该值若小于1,则意味着没有空格;如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;如果该参数没有提供(或者为null),将没有空格。
JSON/JS间数据类型转换
JSON的数据类型与JS非常相识,尤其是基础数据类型和只包含基础数据类型的数组,它们都能相互安全地进行转换,但是对于引用数据类型,要十分小心。
下图是数据转换格式表:
上面这个表中,基本列举了各种数据类型通过stringify转换后的值,但还有一个特殊情况需要注意:对于包含循环引用的对象执行此方法,会抛出错误。
实现
有了上面的分析,现在就有思路来自己实现一个了。首先就是判断传入的值是否是一个引用数据类型,然后根据上面分析的转换结果来进行处理:
// 处理非引用数据类型
if(type !== 'object') {
let result = data;
if (Number.isNaN(data) || data === Infinity) {
//NaN 和 Infinity 序列化返回 "null"
result = "null";
} else if (type === 'function' || type === 'undefined' || type === 'symbol') {
// 由于 function 序列化返回 undefined,因此和 undefined、symbol 一起处理
return undefined;
} else if (type === 'string') {
result = '"' + data + '"';
}
return String(result);
}
然后再处理引用数据类型,由于引用数据类型分内置引用对象和自定义引用对象,所以逻辑更加复杂:
if (type === 'object') {
// 判断null
if (data === null) {
return "null"
} else if (data.toJSON && typeof data.toJSON === 'function') {
// 对象可能内置toJSON方法来自定义序列化对象
return jsonStringify(data.toJSON());
} else if (data instanceof Array) {
let result = [];
//如果是数组,那么数组里面的每一项都需要判断
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 {
// 处理普通对象
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、function、symbol 为属性值,忽略
result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
}
}
});
return ("{" + result + "}").replace(/'/g, '"');
}
}
整体代码:
function jsonStringify(data) {
let type = typeof data;
if(type !== 'object') {
let result = data;
if (Number.isNaN(data) || data === Infinity) {
//NaN 和 Infinity 序列化返回 "null"
result = "null";
} else if (type === 'function' || type === 'undefined' || type === 'symbol') {
// 由于 function 序列化返回 undefined,因此和 undefined、symbol 一起处理
return undefined;
} else if (type === 'string') {
result = '"' + data + '"';
}
return String(result);
} else if (type === 'object') {
if (data === null) {
return "null"
} else if (data.toJSON && typeof data.toJSON === 'function') {
// 对象可能内置toJSON方法来自定义序列化对象
return jsonStringify(data.toJSON());
} else if (data instanceof Array) {
let result = [];
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 {
// 处理普通对象
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、function、symbol 为属性值,忽略
result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
}
}
});
return ("{" + result + "}").replace(/'/g, '"');
}
}
}
总结
从上面代码可以看到,整体逻辑还是十分复杂的,涉及到了多种数据类型,及转换边界,这里总结了几个要注意的地方:
- 根据转换规则,
function会被转换成null,除此之外,undefined和symbol也会被转换为null,因此可以集中一起处理掉。 - 使用
typeof判断 object时,要注意null也是object,因此要对null进行单独判断。 - 对于数组中的元素,有可能是引用数据类型,因此可以对数组元素进行递归判断。