實現深拷貝還在用JSON.parse(JSON.stringify(obj))?帶你用JS實現一個完整版深拷貝函式

MomentYY發表於2022-04-10

使用JavaScript實現深拷貝

1.JSON序列化實現深拷貝

在JS中,想要對某一個物件(引用型別)進行一次簡單的深拷貝,可以使用JSON提供給我們的兩個方法。

  • JSON.stringfy():可以將JavaScript型別轉成對應的JSON字串;
  • JSON.parse():可以解析JSON,將其轉回對應的JavaScript型別;

具體深拷貝的實現:

const obj = {
  name: 'curry',
  age: 30,
  friends: ['kobe', 'klay'],
  playBall() {
    console.log('Curry is playing basketball.')
  }
}

const newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

列印結果:

JSON序列化實現深拷貝的優缺點:

  • 如果只是對一個簡單物件進行深拷貝,那麼使用該方法是很方便的;
  • 但根據上面的列印結果可以發現,原obj的方法屬性並沒有被拷貝到newObj中;
  • JSON序列化只能對普通物件進行深拷貝,如果物件中包含函式、undefined、Symbol等型別的值是無能為力的,會直接將其忽略掉;

2.自定義深拷貝函式

既然上面的方法不能滿足我們的需求,那麼就自己來一步步實現一個深拷貝函式吧。

2.1.基本功能實現

  • 實現深拷貝基本功能,暫時先不對特殊型別進行處理;
  • 定義一個輔助函式isObject,用於判斷傳入資料是否是物件型別;
function isObject(value) {
  const valueType = typeof value
  // 值不能為null,並且為物件或者函式型別
  return (value !== null) && (valueType === 'object' || valueType === 'function')
}
function deepClone(originValue) {
  // 判斷傳入的是否是物件型別,如果不是,說明是普通型別的值,直接返回即可
  if (!isObject(originValue)) {
    return originValue
  }

  const newObj = {} // 定義一個空物件
  // 迴圈遍歷物件,取出key和值存放到空物件中
  // 注意:for...in遍歷物件會將其繼承的屬性也遍歷出來,所以需要加hasOwnProperty進行判斷是否是自身的屬性
  for (const key in originValue) {
    if (originValue.hasOwnProperty(key)) {
      // 遞迴呼叫deepClone,如果物件屬性值中還包含物件,就會再次進行拷貝處理
      newObj[key] = deepClone(originValue[key])
    }
  }

  // 深拷貝完成,將得到新物件返回
  return newObj
}

簡單測試一下:

const obj = {
  name: 'curry',
  age: 30,
  friends: {
    name: 'klay',
    age: 11
  }
}

const newObj = deepClone(obj)
console.log(newObj)
console.log(newObj.friends === obj.friends)

列印結果:

2.2.其他型別處理

  • 對其它資料型別進行處理,如陣列、函式、Symbol、Set、Map等;
  • 對函式型別的判斷,直接返回該函式即可,因為函式本身就是可以複用的;
  • Symbol不僅可以作為value,還可以作為key,需要對key為Symbol型別的情況進行處理;
function deepClone(originValue) {
  // 1.判斷傳入的是否是一個函式型別
  if (typeof originValue === 'function') {
    // 將函式直接返回即可
    return originValue
  }

  // 2.判斷傳入的是否是一個Map型別
  if (originValue instanceof Map) {
    return new Map([...originValue])
  }

  // 3.判斷傳入的是否是一個Set型別
  if (originValue instanceof Set) {
    return new Set([...originValue])
  }

  // 4.判斷傳入的值是否是一個Symbol型別
  if (typeof originValue === 'symbol') {
    // 返回一個新的Symbol,並且將其描述傳遞過去
    return Symbol(originValue.description)
  }

  // 5.判斷傳入的值是否是一個undefined
  if (typeof originValue === 'undefined') {
    return undefined
  }

  // 6.判斷傳入的是否是物件型別,如果不是,說明是普通型別的值,直接返回即可
  if (!isObject(originValue)) {
    return originValue
  }

  // 7.定義一個變數,如果傳入的是陣列就定義為一個陣列
  const newValue = Array.isArray(originValue) ? [] : {}

  // 8.迴圈遍歷,如果是物件,就取出key和值存放到空物件中,如果是陣列,就去除下標和元素放到空陣列中
  // 注意:for...in遍歷物件會將其繼承的屬性也遍歷出來,所以需要加hasOwnProperty進行判斷是否是自身的屬性
  for (const key in originValue) {
    if (originValue.hasOwnProperty(key)) {
      // 遞迴呼叫deepClone,如果物件屬性值中還包含物件,就會再次進行拷貝處理
      newValue[key] = deepClone(originValue[key])
    }
  }

  // 9.對key為Symbol型別的情況進行處理
  // 拿到所有為Symbol型別的key
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  // for...of遍歷取出所有的key,存放到新物件中
  for (const sKey of symbolKeys) {
    newValue[sKey] = deepClone(originValue[sKey])
  }

  // 10.深拷貝完成,將得到新物件返回
  return newValue
}

簡單測試一下:

const s1 = Symbol('aaa')
const s2 = Symbol('bbb')

const obj = {
  name: 'curry',
  age: undefined,
  friends: {
    name: 'klay',
    age: 11
  },
  hobbies: ['籃球', '足球', '高爾夫'],
  map: new Map([[1, 'aaa'], [2, 'bbb'], [3, 'ccc']]),
  set: new Set([1, 2, 3]),
  s: s1,
  [s2]: 'abc'
}

const newObj = deepClone(obj)
console.log(newObj)

列印結果:

2.3.迴圈引用處理

我們自定義深拷貝的函式是通過遞迴來實現的,如果物件中有一個屬性值指向了自己,那麼在進行深拷貝時會陷入無限迴圈,這種情況也就是迴圈引用。

如果沒有處理迴圈引用,那麼就會不斷遞迴,最終報錯棧溢位:

  • 迴圈引用的處理,只需要拿到新建立的物件返回即可,所以必須將這個新物件儲存下來,在遇到迴圈引用屬性時,直接就可以拿到;
  • Map和WeakMap都可以實現對物件進行儲存,這裡使用WeakMap進行儲存,原因是WeakMap對物件的引用是弱引用;
  • 只需要將原物件作為WeakMap中的key,其值對應存放我們新建立出來的物件即可,下一次遞迴時進行判斷WeakMap中是否存有該物件,如果有就取出返回;
function deepClone(originValue, wMap = new WeakMap()) {
  // 1.判斷傳入的是否是一個函式型別
  if (typeof originValue === 'function') {
    // 將函式直接返回即可
    return originValue
  }

  // 2.判斷傳入的是否是一個Map型別
  if (originValue instanceof Map) {
    return new Map([...originValue])
  }

  // 3.判斷傳入的是否是一個Set型別
  if (originValue instanceof Set) {
    return new Set([...originValue])
  }

  // 4.判斷傳入的值是否是一個Symbol型別
  if (typeof originValue === 'symbol') {
    // 返回一個新的Symbol,並且將其描述傳遞過去
    return Symbol(originValue.description)
  }

  // 5.判斷傳入的值是否是一個undefined
  if (typeof originValue === 'undefined') {
    return undefined
  }

  // 6.判斷傳入的是否是物件型別,如果不是,說明是普通型別的值,直接返回即可
  if (!isObject(originValue)) {
    return originValue
  }

  // 迴圈引用處理:判斷wMap中是否存在原物件,如果存在就取出原物件對應的新物件返回
  if (wMap.has(originValue)) {
    return wMap.get(originValue)
  }

  // 7.定義一個變數,如果傳入的是陣列就定義為一個陣列
  const newValue = Array.isArray(originValue) ? [] : {}

  // 迴圈引用處理:將原物件作為key,新物件作為value,存入wMap中
  wMap.set(originValue, newValue)

  // 8.迴圈遍歷,如果是物件,就取出key和值存放到空物件中,如果是陣列,就去除下標和元素放到空陣列中
  // 注意:for...in遍歷物件會將其繼承的屬性也遍歷出來,所以需要加hasOwnProperty進行判斷是否是自身的屬性
  for (const key in originValue) {
    if (originValue.hasOwnProperty(key)) {
      // 遞迴呼叫deepClone,如果物件屬性值中還包含物件,就會再次進行拷貝處理
      newValue[key] = deepClone(originValue[key], wMap)
    }
  }

  // 9.對key為Symbol型別的情況進行處理
  // 拿到所有為Symbol型別的key
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  // for...of遍歷取出所有的key,存放到新物件中
  for (const sKey of symbolKeys) {
    newValue[sKey] = deepClone(originValue[sKey], wMap)
  }

  // 10.深拷貝完成,將得到新物件返回
  return newValue
}

簡單測試一下:

const s1 = Symbol('aaa')
const s2 = Symbol('bbb')

const obj = {
  name: 'curry',
  age: undefined,
  friends: {
    name: 'klay',
    age: 11
  },
  hobbies: ['籃球', '足球', '高爾夫'],
  map: new Map([[1, 'aaa'], [2, 'bbb'], [3, 'ccc']]),
  set: new Set([1, 2, 3]),
  s: s1,
  [s2]: 'abc'
}
// 迴圈引用
obj.self = obj

const newObj = deepClone(obj)
console.log(newObj)
console.log(newObj.self.self.self.self)

列印結果:

相關文章