首頁 > 軟體

JSON stringify及parse方法實現資料深拷貝

2022-08-17 18:02:51

引言

JSON 的 stringifyparse 兩個方法在平時的工作中也很常用,如果沒有一些特殊的型別,是實現資料深拷貝的一個原生方式。

下面就這兩個方法的一個手動實現思路。

JSON.stringify

JSON.stringify 方法用於將 JavaScript 值轉換為 JSON 字串。該方法有三個引數:

  • data: 需要轉換的資料
  • replacer:用於轉換結果的物件或者陣列,可以函數或者陣列
  • space:文字新增縮排、空格和換行符,可以是數位或者字串,數位的最大值是 10,字串的最大長度是 10

下面的測試只用到這些型別:

number,string,function,object,array,null,undefined,map,set,weakmap,weakset

但是 JavaScript 資料的嚴格型別遠遠不止這幾個。

data

首先我們用 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 等都被動的轉換成了空物件。而陣列內的 functionundefined 被替換成了 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 主要是用於新增空格、換行、縮排,但是隻要 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

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
}

JSON.parse

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 }) =&gt; {
  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其它相關文章!


IT145.com E-mail:sddin#qq.com