写一个牛X的JSON.stringify,支持任何类型的js类型!请查收!
原生JSON.stringify支持的类型非常有限
下面具体看一下JSON.stringify到底在哪些类型转换上会有问题,后面都会支持这些类型
- 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值
JSON.stringify(new Number(3)) // ”3“
undefined
、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成null
(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){})
orJSON.stringify(undefined)
.
JSON.stringify({ a: undefined }) // "{}"
JSON.stringify({ a: Symbol() }) // "{}"
JSON.stringify({a: () => {}}) // "{}"
JSON.stringify(undefined) // undefined
JSON.stringify([undefined, ()=>{}, Symbol()]) // "[null,null,null]"
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
const a = {};
a.b = a;
JSON.stringify(a) // Uncaught TypeError: Converting circular structure to JSON
-
所有以 symbol 为属性键的属性都会被完全忽略掉,即便
replacer
参数中强制指定包含了它们。 -
Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
- 这里你可能要问什么事toISOString,ISO时间可以理解为0时区的时间,北京在东八区,所以时间上要+8小时。
// 下面这种就是ISO标准的时间格式 末尾的Z代表东八区
JSON.stringify({ a: new Date() }) // '{"a":"2021-12-22T15:21:52.417Z"}'
// 下面是本地时间,上面的时间+8小时就是下面的时间
new Date() // Wed Dec 22 2021 23:22:09 GMT+0800 (中国标准时间)
- NaN 和 Infinity 格式的数值及 null 都会被当做 null。
JSON.stringify({ a: NaN, B:Infinity }) // '{"a":null,"B":null}'
- 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性
const map = new Map([['A', 1]]);
const set = new Set(['a', 1]);
JSON.stringify({ a: map, b: set }) // '{"a":[["A",1]],"b":["a",1]}'
实现更强大的序列化函数
首先我们实现一个简易版本,让大家知道思路:
在此之前,我们需要了解 JSON.stringify 第二个参数是用来干什么的
-
replacer
可选 -
如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
举例:
function replacer(key, value) {
if (typeof value === "string") {
return undefined;
}
return value;
}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var jsonString = JSON.stringify(foo, replacer); // {"week":45,"month":7}
- 所以我们可以看到,replacer如果判断了value的类型,我们把对应的类型转为字符串是不是就可以了,比如function的类型,我们转为字符串function,当然这里面其实有很多边界判断,我们后面说
function replacer(key, value) {
if (typeof value === "function") {
return value.toString();
}
return value;
}
- 然后parse的时候直接evel('字符串function'),也可以new Function来还原字符串function
我们接着实现怎么还原字符串化完整版的,function序列化
首先要判断:
- 是否是内部函数,比如Array.from,这样的函数序列化要报错,因为我们根本看不到内部函数的代码,例如
'function from() { [native code] }'
// 'function from() { [native code] }'
所以
// 调用函数自身的toString方法,变为字符串
var serializedFn = fn.toString();
var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
// 如果是内部函数就报错
if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) {
throw new TypeError('Serializing native function: ' + fn.name);
}
箭头函数和普通function函数就直接返回字符串
// 调用函数自身的toString方法,变为字符串
var serializedFn = fn.toString();
// 用来校验是否是纯函数,相对于async和generator函数的*而言
var IS_PURE_FUNCTION = /function.*?\(/;
// 判断是否是箭头函数
var IS_ARROW_FUNCTION = /.*?=>.*?/;
// 如果是内部函数就报错
if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) {
throw new TypeError('Serializing native function: ' + fn.name);
}
// 是纯函数就直接返回
if (IS_PURE_FUNCTION.test(serializedFn)) {
return serializedFn;
}
接下来还有一个非常坑的地方,例如这种函数转化
var a = { a:{ key(){} }}
JSON.stringify(a,(key, value)=>{
if(typeof value === 'function'){
return value.toString()
}
return value;
})
// 返回 '{"a":{"key":"key(){}"}}'
eval('(' + '{"a":{"key":"key(){}"}}' + ')')
// a: {key: 'key(){}'}
所以这种情况也要处理,还有就是如果简写es6的函数,还可能加async和gennerator函数
// 调用函数自身的toString方法,变为字符串
var serializedFn = fn.toString();
// 剩下的函数类型就只有三种了
// async函数或者是generator函数,还有最坑的点这种{key() {}}简写函数
var argsStartsAt = serializedFn.indexOf('(');
var def = serializedFn
.substr(0, argsStartsAt)
.trim()
.split(' ')
.filter(function(val) {
return val.length > 0;
});
var nonReservedSymbols = def.filter(function(val) {
return RESERVED_SYMBOLS.indexOf(val) === -1;
});
// 主要是转化这种函数 {key() {}}
if (nonReservedSymbols.length > 0) {
return (
(def.indexOf('async') > -1 ? 'async ' : '') +
'function' +
(def.join('').indexOf('*') > -1 ? '*' : '') +
serializedFn.substr(argsStartsAt)
);
}
我们把最关键的serializeFunc完成了,好了我继续完善这个JSON.stringify的第二个替换函数, 因为下面这个不是正确的思路,所以类型判断这里我就不写详细代码了,大家可以用Object.prototype.toString.call() 结合 typeof 来判断类型
function(
key,
value,
) {
// 如果是date类型
if (type === 'D') {
return 'new Date("' + value.toISOString() + '")';
}
// 如果是正则类型
if (type === 'R') {
return (
'new RegExp(' +
value.source +
', "' +
value.flags +
'")'
);
}
// 如果是map类型
if (type === 'M') {
return (
'new Map(' +
value.entries() +
')'
);
}
// 如果是set类型
if (type === 'S') {
return (
'new Set(' +
Array.from(value.values()) +
')'
);
}
// 如果是array类型
if (type === 'A') {
return (
'Array.prototype.slice.call(' +
Object.assign(
{ length: value.length },
value,
) +
')'
);
}
if (type === 'U') {
return 'undefined';
}
// 如果是number类型
if (type === 'I') {
return value;
}
// 如果是BigInt类型
if (type === 'B') {
return 'BigInt("' + value + '")';
}
// 剩余的认为是function类型
var fn = functions[valueIndex];
return serializeFunc(fn);
}
到此为止,有一个隐藏大问题,那就是被stringify的对象,如果自己包含toJSON方法,那么就以toJSON方法返回值为准,啥意思呢,请看案例:
var obj = {
foo: 'foo',
toJSON: function () {
return 'bar';
}
};
JSON.stringify(obj); // '"bar"'
JSON.stringify({x: obj}); // '{"x":"bar"}'
所以我们以上这种判断有问题,就是人家toJSON已经把你的value都变了,我还在这判断value是啥类型。。。。
到此为止,我们作为学习文章已经算是结束了。相信到这里花不了大家多少时间就看完了,接下来我们看一下完整实现。(此代码是npm 包 serialize-javascript),以下是源码实现,我们上面都已经理清主要思路了,所以源码看起来没啥难度!
'use strict';
var randomBytes = require('randombytes');
// Generate an internal UID to make the regexp pattern harder to guess.
var UID_LENGTH = 16;
var UID = generateUID();
var PLACE_HOLDER_REGEXP = new RegExp('(\\\\)?"@__(F|R|D|M|S|A|U|I|B|L)-' + UID + '-(\\d+)__@"', 'g');
var IS_NATIVE_CODE_REGEXP = /\{\s*\[native code\]\s*\}/g;
var IS_PURE_FUNCTION = /function.*?\(/;
var IS_ARROW_FUNCTION = /.*?=>.*?/;
var UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g;
var RESERVED_SYMBOLS = ['*', 'async'];
// Mapping of unsafe HTML and invalid JavaScript line terminator chars to their
// Unicode char counterparts which are safe to use in JavaScript strings.
var ESCAPED_CHARS = {
'<' : '\\u003C',
'>' : '\\u003E',
'/' : '\\u002F',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
};
function escapeUnsafeChars(unsafeChar) {
return ESCAPED_CHARS[unsafeChar];
}
function generateUID() {
var bytes = randomBytes(UID_LENGTH);
var result = '';
for(var i=0; i<UID_LENGTH; ++i) {
result += bytes[i].toString(16);
}
return result;
}
function deleteFunctions(obj){
var functionKeys = [];
for (var key in obj) {
if (typeof obj[key] === "function") {
functionKeys.push(key);
}
}
for (var i = 0; i < functionKeys.length; i++) {
delete obj[functionKeys[i]];
}
}
module.exports = function serialize(obj, options) {
options || (options = {});
// Backwards-compatibility for `space` as the second argument.
if (typeof options === 'number' || typeof options === 'string') {
options = {space: options};
}
var functions = [];
var regexps = [];
var dates = [];
var maps = [];
var sets = [];
var arrays = [];
var undefs = [];
var infinities= [];
var bigInts = [];
var urls = [];
// Returns placeholders for functions and regexps (identified by index)
// which are later replaced by their string representation.
function replacer(key, value) {
// For nested function
if(options.ignoreFunction){
deleteFunctions(value);
}
if (!value && value !== undefined) {
return value;
}
// If the value is an object w/ a toJSON method, toJSON is called before
// the replacer runs, so we use this[key] to get the non-toJSONed value.
var origValue = this[key];
var type = typeof origValue;
if (type === 'object') {
if(origValue instanceof RegExp) {
return '@__R-' + UID + '-' + (regexps.push(origValue) - 1) + '__@';
}
if(origValue instanceof Date) {
return '@__D-' + UID + '-' + (dates.push(origValue) - 1) + '__@';
}
if(origValue instanceof Map) {
return '@__M-' + UID + '-' + (maps.push(origValue) - 1) + '__@';
}
if(origValue instanceof Set) {
return '@__S-' + UID + '-' + (sets.push(origValue) - 1) + '__@';
}
if(origValue instanceof Array) {
var isSparse = origValue.filter(function(){return true}).length !== origValue.length;
if (isSparse) {
return '@__A-' + UID + '-' + (arrays.push(origValue) - 1) + '__@';
}
}
if(origValue instanceof URL) {
return '@__L-' + UID + '-' + (urls.push(origValue) - 1) + '__@';
}
}
if (type === 'function') {
return '@__F-' + UID + '-' + (functions.push(origValue) - 1) + '__@';
}
if (type === 'undefined') {
return '@__U-' + UID + '-' + (undefs.push(origValue) - 1) + '__@';
}
if (type === 'number' && !isNaN(origValue) && !isFinite(origValue)) {
return '@__I-' + UID + '-' + (infinities.push(origValue) - 1) + '__@';
}
if (type === 'bigint') {
return '@__B-' + UID + '-' + (bigInts.push(origValue) - 1) + '__@';
}
return value;
}
function serializeFunc(fn) {
var serializedFn = fn.toString();
if (IS_NATIVE_CODE_REGEXP.test(serializedFn)) {
throw new TypeError('Serializing native function: ' + fn.name);
}
// pure functions, example: {key: function() {}}
if(IS_PURE_FUNCTION.test(serializedFn)) {
return serializedFn;
}
// arrow functions, example: arg1 => arg1+5
if(IS_ARROW_FUNCTION.test(serializedFn)) {
return serializedFn;
}
var argsStartsAt = serializedFn.indexOf('(');
var def = serializedFn.substr(0, argsStartsAt)
.trim()
.split(' ')
.filter(function(val) { return val.length > 0 });
var nonReservedSymbols = def.filter(function(val) {
return RESERVED_SYMBOLS.indexOf(val) === -1
});
// enhanced literal objects, example: {key() {}}
if(nonReservedSymbols.length > 0) {
return (def.indexOf('async') > -1 ? 'async ' : '') + 'function'
+ (def.join('').indexOf('*') > -1 ? '*' : '')
+ serializedFn.substr(argsStartsAt);
}
// arrow functions
return serializedFn;
}
// Check if the parameter is function
if (options.ignoreFunction && typeof obj === "function") {
obj = undefined;
}
// Protects against `JSON.stringify()` returning `undefined`, by serializing
// to the literal string: "undefined".
if (obj === undefined) {
return String(obj);
}
var str;
// Creates a JSON string representation of the value.
// NOTE: Node 0.12 goes into slow mode with extra JSON.stringify() args.
if (options.isJSON && !options.space) {
str = JSON.stringify(obj);
} else {
str = JSON.stringify(obj, options.isJSON ? null : replacer, options.space);
}
// Protects against `JSON.stringify()` returning `undefined`, by serializing
// to the literal string: "undefined".
if (typeof str !== 'string') {
return String(str);
}
// Replace unsafe HTML and invalid JavaScript line terminator chars with
// their safe Unicode char counterpart. This _must_ happen before the
// regexps and functions are serialized and added back to the string.
if (options.unsafe !== true) {
str = str.replace(UNSAFE_CHARS_REGEXP, escapeUnsafeChars);
}
if (functions.length === 0 && regexps.length === 0 && dates.length === 0 && maps.length === 0 && sets.length === 0 && arrays.length === 0 && undefs.length === 0 && infinities.length === 0 && bigInts.length === 0 && urls.length === 0) {
return str;
}
// Replaces all occurrences of function, regexp, date, map and set placeholders in the
// JSON string with their string representations. If the original value can
// not be found, then `undefined` is used.
return str.replace(PLACE_HOLDER_REGEXP, function (match, backSlash, type, valueIndex) {
// The placeholder may not be preceded by a backslash. This is to prevent
// replacing things like `"a\"@__R-<UID>-0__@"` and thus outputting
// invalid JS.
if (backSlash) {
return match;
}
if (type === 'D') {
return "new Date(\"" + dates[valueIndex].toISOString() + "\")";
}
if (type === 'R') {
return "new RegExp(" + serialize(regexps[valueIndex].source) + ", \"" + regexps[valueIndex].flags + "\")";
}
if (type === 'M') {
return "new Map(" + serialize(Array.from(maps[valueIndex].entries()), options) + ")";
}
if (type === 'S') {
return "new Set(" + serialize(Array.from(sets[valueIndex].values()), options) + ")";
}
if (type === 'A') {
return "Array.prototype.slice.call(" + serialize(Object.assign({ length: arrays[valueIndex].length }, arrays[valueIndex]), options) + ")";
}
if (type === 'U') {
return 'undefined'
}
if (type === 'I') {
return infinities[valueIndex];
}
if (type === 'B') {
return "BigInt(\"" + bigInts[valueIndex] + "\")";
}
if (type === 'L') {
return "new URL(\"" + urls[valueIndex].toString() + "\")";
}
var fn = functions[valueIndex];
return serializeFunc(fn);
});
}
转载自:https://juejin.cn/post/7044828649479995400