<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
JSON 的 stringify
和 parse
兩個方法在平時的工作中也很常用,如果沒有一些特殊的型別,是實現資料深拷貝的一個原生方式。
下面就這兩個方法的一個手動實現思路。
JSON.stringify 方法用於將 JavaScript 值轉換為 JSON 字串。該方法有三個引數:
下面的測試只用到這些型別:
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, indent, replacer, space) break case 'Array': // 處理陣列 res = stringifyArray(data, indent, space) break case 'Number': 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, 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], null, 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, space?: number | string) => { const vals: any[] = [] for (const val of data) { vals.push(stringify(val) || '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,而函數型別有點奇怪,有點不明白,函數的引數是 key 和 value,初始的 key 為空, value 就是當前的物件的值。
所以這裡我們需要修改兩處地方:
export const stringify = (data: any, replacer?: Replacer, space?: number | string, indent = 1) => { // 如果 replacer 為函數的話,直接返回函數執行後的值 if (typeof replacer === 'function') { return replacer('', data) } 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) { const val = stringify(data[key], null, space, indent + 1) if ( val !== undefined && ( // 如果是陣列,則當前的 key 必須是在 replacer 陣列內 !filter || (filter as (string | number)[]).includes(key) || (filter as (string | number)[]).includes(+key) ) ) { vals.push(`"${key}":${space ? ' ' : ''}${val}`) } } // 省略部分程式碼 }
到這裡, stringify 的方法差不多了。下面是完整程式碼:
type Replacer = ((key: string, 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(data[key], null, space, indent + 1) 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, space?: number | string) => { const vals: any[] = [] for (const val of data) { vals.push(stringify(val) || '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) => { if (typeof replacer === 'function') { return replacer('', data) } 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, space) break case 'Number': 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 字串是非常規則的字串,所以我們可以結合正規表示式來提取相關步驟的資料,在字串中的 ' 'tnr
等也是可以的,所以在正則中需要考慮並且替換。
const parse = (data: string | number | null) => { if (typeof data === 'number' || data === null) { 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 = /^(}|])[ tnr]*/.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 parseObjValue = (context: { data: string }) => { const match = /^[ ntr]*((".*?")|([0-9A-Za-z]*))[ tnr]?/.exec(context.data) if (match) { advance(context, match[0].length) const valMatch = /^"(.*?)"$/.exec(match[1]) if (valMatch) { return valMatch[1] } if (match[1] === 'null') { return null } if (isNaN(+match[1])) { throw Error('解析錯誤') } return Number(match[1]) } new Error('解析錯誤') } // 解析陣列值 const parseArrValue = (context: { data: string }) => { const refMatch = /^({|][ ntr]*)/.exec(context.data) // 開啟新的狀態機 if (refMatch) { return parseData(context) } const match = /^((".*?")|([0-9a-zA-Z]*))[ ntr]*[,]?[ ntr]*/.exec(context.data) if (match) { advance(context, match[0].length) const valMatch = /^"(.*?)"$/.exec(match[1]) if (valMatch) { return valMatch[1] } if (match[1] === 'null') { return null } if (isNaN(+match[1])) { throw Error('解析錯誤') } return Number(match[1]) } throw Error('解析錯誤') }
在上面定義狀態的時候,解析物件、陣列和陣列值的時候只有開始狀態,而沒有結束狀態。只是結束狀態統一放入 isEnd 函數中,。
下面開始定義 parseData
函數:
第一步
const parseData = (ctx: { data: string }) => { let res: any = '' let currentState = State.INIT while (isEnd(ctx)) { switch (CurrentState) { case State.INIT: { const match = /^[ tnr]*/.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 = ' tnr{"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 = /^{[ tnr]*/.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 = /^"(.*?)"[ ntr]*:[ ntr]*/.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 = /^[ tnr]*([,}]])[ tnr]*/.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 = /^[[ tnr]*/.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 = /^(}|])[ tnr]*/.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 parseObjValue = (context: { data: string }) => { const match = /^[ ntr]*((".*?")|([0-9A-Za-z]*))[ tnr]?/.exec(context.data) if (match) { advance(context, match[0].length) const valMatch = /^"(.*?)"$/.exec(match[1]) if (valMatch) { return valMatch[1] } if (match[1] === 'null') { return null } if (isNaN(+match[1])) { throw Error('解析錯誤') } return Number(match[1]) } new Error('解析錯誤') } const parseArrValue = (context: { data: string }) => { const refMatch = /^({|][ ntr]*)/.exec(context.data) if (refMatch) { return parseData(context) } const match = /^((".*?")|([0-9a-zA-Z]*))[ ntr]*[,]?[ ntr]*/.exec(context.data) if (match) { advance(context, match[0].length) const valMatch = /^"(.*?)"$/.exec(match[1]) if (valMatch) { return valMatch[1] } if (match[1] === 'null') { return null } if (isNaN(+match[1])) { throw Error('解析錯誤') } return Number(match[1]) } 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 = /^[ tnr]*/.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 = /^{[ tnr]*/.exec(ctx.data) if (match) { advance(ctx, match[0].length) currentState = State.OBJVALSTART } } break case State.OBJVALSTART: { const match = /^"(.*?)"[ ntr]*:[ ntr]*/.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 = /^[ tnr]*([,}]])[ tnr]*/.exec(ctx.data) if (match) { if (match[1] === ',') { currentState = State.OBJVALSTART } advance(ctx, match[0].length) } } break case State.ARRAYSTART: { const match = /^[[ tnr]*/.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) => { if (typeof data === 'number' || data === null) { return data } const context = { data } return parseData(context) }
以上就是JSON stringify及parse方法實現資料深拷貝的詳細內容,更多關於JSON資料深拷貝stringify及parse的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45