您好,登錄后才能下訂單哦!
這篇“JSON.stringify與JSON.parse怎么實現”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“JSON.stringify與JSON.parse怎么實現”文章吧。
JSON 的 stringify
和 parse
兩個方法在平時的工作中也很常用,如果沒有一些特殊的類型,是實現數據深拷貝的一個原生方式。
下面就這兩個方法的一個手動實現思路。
JSON.stringify 方法用于將 JavaScript 值轉換為 JSON 字符串。該方法有三個參數:
data: 需要轉換的數據
replacer:用于轉換結果的對象或者數組,可以函數或者數組
space:文本添加縮進、空格和換行符,可以是數字或者字符串,數字的最大值是 10,字符串的最大長度是 10
下面的測試只用到這些類型: number,string,function,object,array,null,undefined,map,set,weakmap,weakset
但是 JavaScript 數據的嚴格類型遠遠不止這幾個。
首先我們用 JSON.stringify 來打印結果:
const testJson = { 4: 3, n: 1, s: 's', f: () => { }, null: null, unde: undefined, arr: [1, 's', null, undefined, () => { }], obj: { n: '1', s: 's' }, map: new Map(), set: new Set([1, 2, 3]), wmap: new WeakMap(), wset: new WeakSet() } const raws = JSON.stringify(testJson) // { // "4":3,"n":1,"s":"s","null":null,"arr":[1,"s",null,null,null], // "obj":{"n":"1","s":"s"},"map":{},"set":{},"wmap":{},"wset":{} // }
根據上面的結果,我們可以發現對象內的 function
, undefined
被剔除了,map
, set
等都被動的轉換成了空對象。而數組內的 function
和 undefined
被替換成了 null
。
所以我們可以根據上述規則寫一個簡單的 stringify
方法:
const stringify = (data: any) => { // 獲取數據的嚴格類型 const type = getType(data) let res = '' switch (type) { case 'Object': // 處理對象 res = stringifyObject(data) break case 'Array': // 處理數組 res = stringifyArray(data) break case 'Number': case 'Boolean': res = `${data}` break case 'String': res = `"${data}"` break case 'Null': res = 'null' break case 'Set': case 'WeakSet': case 'Map': case 'WeakMap': res = '{}' break default: return } return res }
實現幾個輔助函數:
// 獲取嚴格類型 const getType = (data: any) => { return Object.prototype.toString.call(data).slice(8, -1) } // 處理對象方法 const stringifyObject = (data: Record<string, any>) => { const vals: string[] = [] for (const key in data) { // 遞歸處理 const val = stringify(data[key]) // 如果值為 undefined,我們則需要跳過 if (val !== undefined) { vals.push(`"${key}":${val}`) } } return `{${vals.join(',')}}` } // 處理數組方法 const stringifyArray = (data: any[]) => { const vals: any[] = [] for (const val of data) { // 遞歸處理,如果返回 undefined 則替換為 null vals.push(stringify(val) || 'null') } return `[${vals.join(',')}]` }
到這里就實現了 stringify
的簡單版本。下面可以簡單測試一下:
const raws = JSON.stringify(testJson) const cuss = stringify(testJson) console.log(raws === cuss) // true
后面還有兩個參數,我們先實現第三個,第二個參數的作用等下在實現。
space 主要是用于添加空格、換行、縮進,但是只要 space 的值是合法的,換行符是默認加上一個的。所以我們要改下 stringify 的方法:
type Replacer = ((key: string, value: any) => any) | null | (string | number)[] export const stringify = (data: any, replacer?: Replacer, space?: number | string, indent = 1) => { const type = getType(data) if (typeof space === 'number') { if (space <= 0) { space = undefined } else { space = Math.min(10, space) } } else if (typeof space === 'string') { space = space.substring(0, 10) } else if (space) { space = undefined } let res = '' switch (type) { case 'Object': res = stringifyObject(data, indent, replacer, space) break case 'Array': res = stringifyArray(data, indent, replacer, space) break // 省略部分代碼 } // 省略部分代碼 }
對于 space 的不同非法的值,我們可以在控制臺上進行一些簡單的測試就可以得出,像 -1
這種其實是不生效的。而我處理的是只能是數字和字符串,數字必須是 1 - 10,字符串的最長長度是 10 位,其余的都重置為 undefined。因為像數組和對象的這種嵌套,縮進其實是要跟著動的,這里就新增了 indent
字段,初始為 1,后續遞歸就 + 1。
// 新增分隔符處理方法 const handleSeparator = (space: number | string, indent: number, prefix: string = '', suffix: string = '') => { let separator = prefix + '\n' if (typeof space === 'number') { separator += ' '.repeat(space).repeat(indent) } else { separator += space.repeat(indent) } return separator + suffix } // 對象方法修改 const stringifyObject = (data: Record<string, any>, indent: number, replacer?: Replacer, space?: number | string) => { const vals: string[] = [] for (const key in data) { const val = stringify(data[key], replacer, space, indent + 1) if (val !== undefined) { vals.push(`"${key}":${space ? ' ' : ''}${val}`) } } // 新增 space 處理 if (space) { const val = vals.join(handleSeparator(space, indent, ',')) if (!val) { return '{}' } const front = handleSeparator(space, indent, '{') const back = handleSeparator(space, indent - 1, '', '}') return front + val + back } return `{${vals.join(',')}}` } // 數組處理方法 const stringifyArray = (data: any[], indent: number, replacer?: Replacer, space?: number | string) => { const vals: any[] = [] for (const val of data) { vals.push(stringify(val, replacer, space, indent + 1) || 'null') } // 新增 space 處理 if (space) { const front = handleSeparator(space, indent, '[') const val = vals.join(handleSeparator(space, indent, ',')) const back = handleSeparator(space, indent - 1, '', ']') return front + val + back } return `[${vals.join(',')}]` }
replacer 參數有兩個類型:
數組類型是用來過濾對象類型內的字段,只保留數組內的 key
函數類型就是在數組和對象遍歷的時候,開發者可以自定義某些類型的字符串方式
所以這里我們需要修改三處地方:
// 起始方法增加一個是否是第一次調用的標記 init export const stringify = (data: any, replacer?: Replacer, space?: number | string, indent = 1, init = true) => { // 如果 replacer 為函數的話,初始 key 為空串,值為 data if (typeof replacer === 'function' && init) { return stringify(replacer('', data), replacer, space, indent, false) } const type = getType(data) // 省略部分代碼 } const stringifyObject = (data: Record<string, any>, indent: number, replacer?: Replacer, space?: number | string) => { const filter = getType(replacer) === 'Array' ? replacer : null const vals: string[] = [] for (const key in data) { // 補全參數,修改 replacer 處理 const val = stringify( typeof replacer === 'function' ? replacer(key, data[key]) : data[key], replacer, space, indent + 1, false ) if ( val !== undefined && ( !filter || (filter as (string | number)[]).includes(key) || (filter as (string | number)[]).includes(+key) ) ) { vals.push(`"${key}":${space ? ' ' : ''}${val}`) } } // 省略部分代碼 } const stringifyArray = (data: any[], indent: number, replacer?: Replacer, space?: number | string) => { const vals: any[] = [] let i = 0 for (const val of data) { // 補全參數,修改 replacer 處理 vals.push(stringify( typeof replacer === 'function' ? replacer(i++, val) : val, replacer, space, indent + 1, false ) || 'null') } if (space) { const front = handleSeparator(space, indent, '[') const val = vals.join(handleSeparator(space, indent, ',')) const back = handleSeparator(space, indent - 1, '', ']') return front + val + back } return `[${vals.join(',')}]` }
到這里, stringify 的方法差不多了。下面是完整代碼:
type Replacer = ((key: string | number, value: any) => any) | null | (string | number)[] const getType = (data: any) => { return Object.prototype.toString.call(data).slice(8, -1) } const handleSeparator = (space: number | string, indent: number, prefix: string = '', suffix: string = '') => { let separator = prefix + '\n' if (typeof space === 'number') { separator += ' '.repeat(space).repeat(indent) } else { separator += space.repeat(indent) } return separator + suffix } const stringifyObject = (data: Record<string, any>, indent: number, replacer?: Replacer, space?: number | string) => { const filter = getType(replacer) === 'Array' ? replacer : null const vals: string[] = [] for (const key in data) { const val = stringify( typeof replacer === 'function' ? replacer(key, data[key]) : data[key], replacer, space, indent + 1, false ) if ( val !== undefined && ( !filter || (filter as (string | number)[]).includes(key) || (filter as (string | number)[]).includes(+key) ) ) { vals.push(`"${key}":${space ? ' ' : ''}${val}`) } } if (space) { const val = vals.join(handleSeparator(space, indent, ',')) if (!val) { return '{}' } const front = handleSeparator(space, indent, '{') const back = handleSeparator(space, indent - 1, '', '}') return front + val + back } return `{${vals.join(',')}}` } const stringifyArray = (data: any[], indent: number, replacer?: Replacer, space?: number | string) => { const vals: any[] = [] let i = 0 for (const val of data) { vals.push(stringify( typeof replacer === 'function' ? replacer(i++, val) : val, replacer, space, indent + 1, false ) || 'null') } if (space) { const front = handleSeparator(space, indent, '[') const val = vals.join(handleSeparator(space, indent, ',')) const back = handleSeparator(space, indent - 1, '', ']') return front + val + back } return `[${vals.join(',')}]` } export const stringify = (data: any, replacer?: Replacer, space?: number | string, indent = 1, init = true): string | undefined => { if (typeof replacer === 'function' && init) { return stringify(replacer('', data), replacer, space, indent, false) } const type = getType(data) if (typeof space === 'number') { if (space <= 0) { space = undefined } else { space = Math.min(10, space) } } else if (typeof space === 'string') { space = space.substring(0, 10) } else if (space) { space = undefined } let res = '' switch (type) { case 'Object': res = stringifyObject(data, indent, replacer, space) break case 'Array': res = stringifyArray(data, indent, replacer, space) break case 'Number': res = `${data}` break case 'Boolean': res = `${data}` break case 'String': res = `"${data}"` break case 'Null': res = 'null' break case 'Set': case 'WeakSet': case 'Map': case 'WeakMap': res = '{}' break default: return } return res }
stringify
方法的實現還是比較簡單的,在一些筆試中還有可能會有相關需要實現的題。
而 JSON.parse
則是需要將合法的 json 字符串轉換成對象,這里就需要用到一個概念:有限狀態自動機
這里只做簡單的介紹:有限狀態機(Finite State Machine),是指任意時刻都處于有限狀態集合中的某一狀態。當其獲得一個輸入字符時,將從當前狀態轉換到另一個狀態或者仍然保持當前狀態。
可以結合當前 json 字符串的場景來簡單理解一下:
我們有如下一個字符串:
const str = '{"4":3,"s":"s","null":null,"arr":[1,"s",null],"obj":{}}'
然后定義幾個狀態:
const State = { INIT: 'INIT', // 初始狀態 OBJECTSTART: 'OBJECTSTART', // 開始解析對象 ARRAYSTART: 'ARRAYSTART', // 開始解析數組 OBJVALSTART: 'OBJVALSTART', // 開始解析對象的屬性與值 OBJVALEND: 'OBJVALEND', // 對象屬性與值解析結束 ARRVALSTART: 'ARRVALSTART' // 開始解析數組值 }
因為 json 字符串是非常規則的字符串,所以我們可以結合正則表達式來提取相關步驟的數據,在字符串中的 ' '\t\n\r
等也是可以的,所以在正則中需要考慮并且替換。
const parse = (data: string | number | null || boolean) => { if (typeof data === 'number' || data === null || typeof data === 'boolean') { return data } // 將字符串轉換為地址引用,方便后面字符串數據的消費 const context = { data } // 具體解析方法 return parseData(context) }
然后定義幾個輔助函數:
// 字符串的消費函數 - 就是截取已匹配完的數據,返回剩余字符串 const advance = (context: { data: string }, num: number) => { context.data = context.data.slice(num) } // 是否結束狀態機 const isEnd = (ctx: { data: string }) => { // 如果沒有數據了,則結束 if (!ctx.data) { return false } const match = /^([}\]])[ \t\n\r]*/.exec(ctx.data) if (match) { if ( match[1] === '}' && getType(res) !== 'Object' || match[1] === ']' && getType(res) !== 'Array' ) { throw Error('解析錯誤') } advance(ctx, match[0].length) return false } return true } // 處理值 const parseValue = (context: { data: string }, match: any[]) => { advance(context, match[0].length) const valMatch = /^"(.*?)"$/.exec(match[1]) if (valMatch) { return valMatch[1] } if (match[1] === 'null') { return null } if (match[1] === 'true') { return true } if (match[1] === 'false') { return false } if (isNaN(+match[1])) { throw Error('解析錯誤') } return Number(match[1]) } // 解析對象屬性值 const parseObjValue = (context: { data: string }) => { const match = /^[ \n\t\r]*((".*?")|([0-9A-Za-z]*))[ \t\n\r]*/.exec(context.data) if (match) { return parseValue(context, match) } new Error('解析錯誤') } // 解析數組值 const parseArrValue = (context: { data: string }) => { const refMatch = /^({|\[[ \n\t\r]*)/.exec(context.data) if (refMatch) { return parseData(context) } const match = /^((".*?")|([0-9a-zA-Z]*))[ \n\t\r]*[,]?[ \n\t\r]*/.exec(context.data) if (match) { return parseValue(context, match) } throw Error('解析錯誤') }
在上面定義狀態的時候,解析對象、數組和數組值的時候只有開始狀態,而沒有結束狀態。只是結束狀態統一放入 isEnd 函數中,。
下面開始定義 parseData
函數:
第一步
const parseData = (ctx: { data: string }) => { let res: any = '' let currentState = State.INIT while (isEnd(ctx, res)) { switch (currentState) { case State.INIT: { const match = /^[ \t\n\r]*/.exec(ctx.data) if (match?.[0].length) { advance(ctx, match[0].length) } if (ctx.data[0] === '{') { res = {} currentState = State.OBJECTSTART } else if (ctx.data[0] === '[') { res = [] currentState = State.ARRAYSTART } else { res = parseObjValue(ctx) } } break case State.OBJECTSTART: break case State.OBJVALSTART: break case State.OBJVALEND: break case State.ARRAYSTART: break case State.ARRVALSTART: break // no default } } return res }
INIT
中,先去掉前面的空格、換行等字符,示例:
// 消費前 const str1 = ' \t\n\r{"4":3,"s":"s","null":null,"arr":[1,"s",null],"obj":{}}' // 消費后 const str2 = '{"4":3,"s":"s","null":null,"arr":[1,"s",null],"obj":{}}'
然后再判讀第一個字符是什么:
如果是 {
,則將狀態轉移到 OBJECTSTART
,將 res
賦值一個空對象
如果是 [
,則將狀態轉移到 ARRAYSTART
,將 res
賦值一個空數組
如果都不是,則就是一個值,可以用對象解析屬性值的方法來解析,判讀是否是合法的字符串
所以這里的狀態轉移到了對象解析 OBJECTSTART
:
第二步
const parseData = (ctx: { data: string }) => { let res: any = '' let currentState = State.INIT while (isEnd(ctx)) { switch (currentState) { case State.INIT: // 省略部分代碼 break case State.OBJECTSTART: { const match = /^{[ \t\n\r]*/.exec(ctx.data) if (match) { advance(ctx, match[0].length) currentState = State.OBJVALSTART } } break case State.OBJVALSTART: break case State.OBJVALEND: break case State.ARRAYSTART: break case State.ARRVALSTART: break // no default } } return res }
OBJECTSTART
中,消費掉 {
,將狀態轉移到 OBJVALSTART
, 剩余字符數據:
const str = '"4":3,"s":"s","null":null,"arr":[1,"s",null],"obj":{}}'
第三步
const parseData = (ctx: { data: string }) => { let res: any = '' let currentState = State.INIT while (isEnd(ctx)) { switch (currentState) { case State.INIT: // 省略部分代碼 break case State.OBJECTSTART: // 省略部分代碼 break case State.OBJVALSTART: { const match = /^"(.*?)"[ \n\t\r]*:[ \n\t\r]*/.exec(ctx.data) if (match) { advance(ctx, match[0].length) if (ctx.data[0] === '{' || ctx.data[0] === '[') { res[match[1]] = parseData(ctx) } else { res[match[1]] = parseObjValue(ctx) } currentState = State.OBJVALEND } } break case State.OBJVALEND: break case State.ARRAYSTART: break case State.ARRVALSTART: break // no default } } return res }
先獲取 key:
等數組并消費,剩余字符數據:
const str = '3,"s":"s","null":null,"arr":[1,"s",null],"obj":{}}'
先判讀后續字符的第一個字符是什么:
如果是 {
或者 [
,則開啟一個新的狀態機
否則直接用 parseObjValue
解析值
最后將狀態轉移至 OBJVALEND
。
第四步
const parseData = (ctx: { data: string }) => { let res: any = '' let currentState = State.INIT while (isEnd(ctx)) { switch (currentState) { case State.INIT: // 省略部分代碼 break case State.OBJECTSTART: // 省略部分代碼 break case State.OBJVALSTART: // 省略部分代碼 break case State.OBJVALEND: { const match = /^[ \t\n\r]*(,)[ \t\n\r]*/.exec(ctx.data) if (match) { if (match[1] === ',') { currentState = State.OBJVALSTART } advance(ctx, match[0].length) } } break case State.ARRAYSTART: break case State.ARRVALSTART: break // no default } } return res }
如果后面匹配出來的字符是 ,
,則表示后續還有其它的對象屬性,我們需要將狀態重新轉移到 OBJVALSTART
, 如果是其它的 }
或者 ]
,則會在此次消費完畢,然后在 isEnd
中會退出狀態機。
后續剩余字符的變化會依照上數狀態的變化而進行字符消費:
const str = '3,"s":"s","null":null,"arr":[1,"s",null],"obj":{}}' // 1 const str = ',"s":"s","null":null,"arr":[1,"s",null],"obj":{}}' // 2 const str = '"s":"s","null":null,"arr":[1,"s",null],"obj":{}}' // 省略 s 和 null // 3 開啟新的狀態機 const str = '[1,"s",null],"obj":{}}' // 4 結束狀態機 const str = '],"obj":{}}' // 5 開啟新的狀態機 const str = '{}}' // 6 結束狀態機 const str = '}}' // 7 結束狀態機 const str = '}'
數組的處理
const parseData = (ctx: { data: string }) => { let res: any = '' let currentState = State.INIT while (isEnd(ctx)) { switch (currentState) { case State.INIT: // 省略部分代碼 break case State.OBJECTSTART: // 省略部分代碼 break case State.OBJVALSTART: // 省略部分代碼 break case State.OBJVALEND: // 省略部分代碼 break case State.ARRAYSTART: { const match = /^\[[ \t\n\r]*/.exec(ctx.data) if (match) { advance(ctx, match[0].length) currentState = State.ARRVALSTART } } break case State.ARRVALSTART: res.push(parseArrValue(ctx)) break // no default } } return res }
如果第一個字符為 [
,則會開啟新的狀態機,狀態也會轉換為 ARRAYSTART
,然后在 ARRAYSTART
狀態內進行數組值的轉換。
到這里整個 JSON.parse
的實現思路差不多,但是上述的流程應該有沒考慮到的地方,但是大體差不多,只是邊界的處理問題。測試示例:
// 數據使用上面的 testJson const raws = JSON.stringify(testJson) const rawp = JSON.parse(raws) const cusp = parse(raws) console.log(raws, 'JSON.stringify') console.log(rawp, 'JSON.parse') console.log(cusp, 'parse')
結果:
const State = { INIT: 'INIT', OBJECTSTART: 'OBJECTSTART', ARRAYSTART: 'ARRAYSTART', OBJVALSTART: 'OBJVALSTART', OBJVALEND: 'OBJVALEND', ARRVALSTART: 'ARRVALSTART' } const isEnd = (ctx: { data: string }, res: any) => { if (!ctx.data) { return false } const match = /^([}\]])[ \t\n\r]*/.exec(ctx.data) if (match) { if ( match[1] === '}' && getType(res) !== 'Object' || match[1] === ']' && getType(res) !== 'Array' ) { throw Error('解析錯誤') } advance(ctx, match[0].length) return false } return true } const advance = (context: { data: string }, num: number) => { context.data = context.data.slice(num) } const parseValue = (context: { data: string }, match: any[]) => { advance(context, match[0].length) const valMatch = /^"(.*?)"$/.exec(match[1]) if (valMatch) { return valMatch[1] } if (match[1] === 'null') { return null } if (match[1] === 'true') { return true } if (match[1] === 'false') { return false } if (isNaN(+match[1])) { throw Error('解析錯誤') } return Number(match[1]) } const parseObjValue = (context: { data: string }) => { const match = /^[ \n\t\r]*((".*?")|([0-9A-Za-z]*))[ \t\n\r]*/.exec(context.data) if (match) { return parseValue(context, match) } new Error('解析錯誤') } const parseArrValue = (context: { data: string }) => { const refMatch = /^({|\[[ \n\t\r]*)/.exec(context.data) if (refMatch) { return parseData(context) } const match = /^((".*?")|([0-9a-zA-Z]*))[ \n\t\r]*[,]?[ \n\t\r]*/.exec(context.data) if (match) { return parseValue(context, match) } throw Error('解析錯誤') } const parseData = (ctx: { data: string }) => { let res: any = '' let currentState = State.INIT while (isEnd(ctx, res)) { switch (currentState) { case State.INIT: { const match = /^[ \t\n\r]*/.exec(ctx.data) if (match?.[0].length) { advance(ctx, match[0].length) } if (ctx.data[0] === '{') { res = {} currentState = State.OBJECTSTART } else if (ctx.data[0] === '[') { res = [] currentState = State.ARRAYSTART } else { res = parseObjValue(ctx) } } break case State.OBJECTSTART: { const match = /^{[ \t\n\r]*/.exec(ctx.data) if (match) { advance(ctx, match[0].length) currentState = State.OBJVALSTART } } break case State.OBJVALSTART: { const match = /^"(.*?)"[ \n\t\r]*:[ \n\t\r]*/.exec(ctx.data) if (match) { advance(ctx, match[0].length) if (ctx.data[0] === '{' || ctx.data[0] === '[') { res[match[1]] = parseData(ctx) } else { res[match[1]] = parseObjValue(ctx) } currentState = State.OBJVALEND } } break case State.OBJVALEND: { const match = /^[ \t\n\r]*(,)[ \t\n\r]*/.exec(ctx.data) if (match) { if (match[1] === ',') { currentState = State.OBJVALSTART } advance(ctx, match[0].length) } } break case State.ARRAYSTART: { const match = /^\[[ \t\n\r]*/.exec(ctx.data) if (match) { advance(ctx, match[0].length) currentState = State.ARRVALSTART } } break case State.ARRVALSTART: res.push(parseArrValue(ctx)) break // no default } } return res } export const parse = (data: string | number | null | boolean) => { if (typeof data === 'number' || data === null || typeof data === 'boolean') { return data } const context = { data } return parseData(context) }
以上就是關于“JSON.stringify與JSON.parse怎么實現”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。