JavaScript 的淺拷貝和深拷貝

什麼是淺拷貝和深拷貝

  • 拷貝:指拷貝源對象到目標對象,又分爲淺拷貝和深拷貝兩種
  • 淺拷貝:如拷貝的對象有屬性值是非基礎類型(即對象),則淺拷貝拷貝的是對象的引用,而非對象自己,拷貝完成之後更改目標對象,源對象也會被更改
  • 深拷貝:深拷貝完美解決了淺拷貝存在的問題,目標對象是一個全新的對象,更改目標對象不會影響到源對象

淺拷貝

Object.assign()

// 對象的屬性值都是基礎類型
console.log('--------對象的屬性值都是基礎類型--------')
const src1 = { a: 'aa' }
const target1 = Object.assign({}, src1)
// 輸出源對象
console.log('源對象: ', src1)  // {a: "aa"}
// 輸出拷貝後的目標對象
console.log('目標對象: ', target1)  // {a: "aa"}
// 更改目標對象
target1.a = 'aaa'
console.log('更改目標對象後的結果: ')
// 輸出更改後的目標對象
console.log('目標對象: ', target1)  // {a: "aaa"}
// 輸出源對象,發現源對象沒有被改變
console.log('源對象: ', src1)  // {a: "aa"}

// 對象的屬性值爲非基礎類型
console.log('--------對象的屬性值爲非基礎類型--------')
const src2 = {flag: 'src2', a: { b: 'bb' } }
const target2 = Object.assign({}, src2)
// 輸出源對象
console.log('源對象: ', src2)  // {flag: "src2", a: {b: "bb"}}
// 輸出目標對象
console.log('目標對象: ', target2)  // {flag: "src2", a: {b: "bb"}}
// 更改目標對象
target2.flag = 'target2'
target2.a.b = 'bbb'
console.log('更改目標對象後的結果: ')
// 輸出更改後的目標對象
console.log('目標對象: ', target2)  // {flag: "target2", a: {b: "bbb"}}
// 輸出源對象,發現源對象被改了
console.log('源對象: ', src2)  // {flag: "src2", a: {b: "bbb"}}

in運算符

// 經過for in循環複製對象
function copy (target, src) {
  for (let key in src) {
    // 過濾掉原型鏈上的屬性,只複製src對象自身的屬性和值
    if (src.hasOwnProperty(key)) {
      target[key] = src[key]
    }
  }
  return target
}
// 對象的屬性值都是基礎類型
console.log('--------對象的屬性值都是基礎類型--------')
const src1 = { a: 'aa' }
const target1 = copy({}, src1)
// 輸出源對象
console.log('源對象: ', src1)  // {a: "aa"}
// 輸出拷貝後的目標對象
console.log('目標對象: ', target1)  // {a: "aa"}
// 更改目標對象
target1.a = 'aaa'
console.log('更改目標對象後的結果: ')
// 輸出更改後的目標對象
console.log('目標對象: ', target1)  // {a: "aaa"}
// 輸出源對象,發現源對象沒有被改變
console.log('源對象: ', src1)  // {a: "aa"}

// 對象的屬性值爲非基礎類型
console.log('--------對象的屬性值爲非基礎類型--------')
const src2 = {flag: 'src2', a: { b: 'bb' } }
const target2 = copy({}, src2)
// 輸出源對象
console.log('源對象: ', src2)  // {flag: "src2", a: {b: "bb"}}
// 輸出目標對象
console.log('目標對象: ', target2)  // {flag: "src2", a: {b: "bb"}}
// 更改目標對象
target2.flag = 'target2'
target2.a.b = 'bbb'
console.log('更改目標對象後的結果: ')
// 輸出更改後的目標對象
console.log('目標對象: ', target2) // {flag: "target2", a: {b: "bbb"}}
// 輸出源對象,發現源對象被改了
console.log('源對象: ', src2)  // {flag: "src2", a: {b: "bbb"}}

深拷貝

JSON

  • 優勢

javascript的內置方法,簡單、性能好
  • 缺點

  • 會忽略掉對象中屬性值爲undefined和函數的屬性
  • 因爲方法的底層實現用了遞歸,若是對象存在循環引用,會爆棧(報循環引用的錯)
  • 實現

// 直接上對象的屬性值爲非基礎類型的對象
const src = { flag: 'src', a: { b: 'bb' } }
console.log('源對象: ', src)  // 源對象:  {flag: "src", a: {b: "bb"}}
const target = JSON.parse(JSON.stringify(src))
console.log('目標對象: ', target) // 目標對象: {flag: "src", a: {b: "bb"}}
console.log('--------更改目標對象--------')
target.flag = 'target'
target.a.b = 'bbb'
console.log('目標對象: ', target)  // 目標對象:  {flag: "target", a: {b: "bbb"}}
// 發現源對象沒有被改變
console.log('源對象: ', src)  // 源對象: {flag: "src", a: {b: "bb"}}

// 異常狀況 - 屬性值爲undefined和function的狀況
const obj = { a: 'a', b: undefined, c: function () { conole.log('c function') }}
console.log('源對象: ', obj)  // 源對象:  {a: "a", b: undefined, c: ƒ}
const copyObj = JSON.parse(JSON.stringify(obj))
// 對象的b和c屬性不見了
console.log('拷貝後的目標對象: ', copyObj)  // 拷貝後的目標對象:  {a: "a"}

// 異常狀況 - 對象存在循環引用
const objLoop = {}
const b = {objLoop}
objLoop.b = b
console.log('源對象: ', objLoop) // {b: {objLoop: {b: {objLoop: {b: ...}}}}}
// 報錯: Uncaught TypeError: Converting circular structure to JSON
const copyObjLoop = JSON.parse(JSON.stringify(objLoop))

MessageChannel

  • 優勢

JavaScript的內置API,簡單,且能夠複製屬性值爲undefined的對象,也能夠解決循環引用的問題
  • 缺點

  • 對象有屬性值爲函數時會報錯
  • 方法是異步的
  • 實現

// 拷貝方法
function deepCopy (obj) {
  return new Promise((resolve, reject) => {
    const {port1, port2} = new MessageChannel()
    port1.postMessage(obj)
    port2.onmessage = function(e) {
      resolve(e.data)
    }
  })
}

// 示例一,正常對象
// 源對象
let src = { flag: 'src', a: { b: 'bb' } }
// 目標對象
let target = {}
deepCopy(src).then(res => {
  target = res
  console.log('源對象: ', src)  // 源對象:  {flag: "src", a: {b: "bb"}}
  console.log('目標對象: ', target) // 目標對象: {flag: "src", a: {b: "bb"}}
  console.log('--------更改目標對象--------')
  target.flag = 'target'
  target.a.b = 'bbb'
  console.log('目標對象: ', target)  // 目標對象:  {flag: "target", a: {b: "bbb"}}
  // 發現源對象沒有被改變
  console.log('源對象: ', src)  // 源對象: {flag: "src", a: {b: "bb"}}
})

// 示例二,屬性值爲undefined的狀況
const obj = { a: 'a', b: undefined}
target = {}
deepCopy(obj).then(res => {
  console.log('源對象: ', obj)  // 源對象:  {a: "a", b: undefined, c: ƒ}
  target = res
  console.log('拷貝後的目標對象: ', target)  // 拷貝後的目標對象:  {a: "a", b: undefined}
})

// 示例三, 屬性值有函數時,會報錯: Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'MessagePort': function () {} could not be cloned.
/*
const obj1 = { a: 'a', b: undefined, c: function () {}}
target = {}
deepCopy(obj1).then(res => {
  console.log('源對象: ', obj)  // 源對象:  {a: "a", b: undefined, c: ƒ}
  target = res
  console.log('拷貝後的目標對象: ', target)
})
*/

// 示例四,循環引用
const obj2 = {}
const b = {obj2}
obj2.b = b
target = {}
deepCopy(obj2).then(res => {
  console.log('源對象: ', obj2) // {b: {obj2: {b: {obj2: {b: ...}}}}}
  target = res
  console.log('目標對象: ', target) // {b: {obj2: {b: {obj2: {b: ...}}}}}
})

遞歸

  • 優勢

能夠解決JSON方式忽略屬性值爲undefined和function的屬性的問題
  • 缺點

對象存在循環引用時仍然會爆棧
  • 實現

// 深拷貝方法
function deepCopy (obj) {
  const target = {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // key爲對象自身可枚舉的屬性
      if (Object.prototype.toString.call(obj[key]) === '[object Object]') {
        // 屬性值爲對象,遞歸調用
        target[key] = deepCopy(obj[key])
      } else {
        target[key] = obj[key]
      }
    }
  }
  return target
}

// 示例一
const obj1 = {a: 'a', b: {c: 'cc'}}
console.log('源對象: ', obj1) // 源對象:  {a: "a", b: {c: "cc"}}
const copyObj1 = deepCopy(obj1)
console.log('拷貝後的目標對象: ', copyObj1) // 拷貝後的目標對象:  {a: "a", b: {c: "cc"}}
console.log('-----改變目標對象的屬性---------')
copyObj1.b = 'bb'
console.log('源對象: ', obj1) // 源對象:  {a: "a", b: {c: "cc"}}
console.log('目標對象: ', copyObj1) // 目標對象:  {a: "a", b: "bb"}

// 示例二, 對象存在循環引用
const objLoop = {}
const b = {objLoop}
objLoop.b = b
console.log('源對象: ', objLoop) // {b: {objLoop: {b: {objLoop: {b: ...}}}}}
// 報錯: Uncaught RangeError: Maximum call stack size exceeded
const copyObjLoop = deepCopy(objLoop)

閉包 + 遞歸

  • 說明

解決了JSON方式和遞歸方式存在的問題,可實現真正的深拷貝
  • 實現

// 深拷貝方法
function deepCopy (copyObj) {
  // 用來記錄已經拷貝過的屬性值爲對象的屬性以及屬性的值,解決遞歸循環引用對象的爆棧問題
  const cache = {}

  // 拷貝對象
  function copy (obj) {
    const target = {}
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        // key爲對象自身可枚舉的屬性
        if (Object.prototype.toString.call(obj[key]) === '[object Object]') {
          // 屬性值爲對象
          if (cache[obj[key]]) {
            // 說明該屬性已經被拷貝過一次,如今又拷貝,證實出現了循環引用
            target[key] = cache[obj[key]]
          } else {
            cache[obj[key]] = obj[key]
            target[key] = copy(obj[key])
          }
        } else {
          target[key] = obj[key]
        }
      }
    }
    return target
  }
  return copy(copyObj)
}


// 示例一
const obj1 = {a: 'a', b: {c: 'cc'}}
console.log('源對象: ', obj1) // 源對象:  {a: "a", b: {c: "cc"}}
const copyObj1 = deepCopy(obj1)
console.log('拷貝後的目標對象: ', copyObj1) // 拷貝後的目標對象:  {a: "a", b: {c: "cc"}}
console.log('-----改變目標對象的屬性---------')
copyObj1.b = 'bb'
console.log('源對象: ', obj1) // 源對象:  {a: "a", b: {c: "cc"}}
console.log('目標對象: ', copyObj1) // 目標對象:  {a: "a", b: "bb"}

// 示例二, 對象存在循環引用
const objLoop = {}
const b = {objLoop}
objLoop.b = b
console.log('源對象: ', objLoop) // 源對象: {b: {objLoop: {b: {objLoop: {b: ...}}}}}
const copyObjLoop = deepCopy(objLoop)
console.log('目標對象: ', copyObjLoop) // 目標對象: {b: {objLoop: {b: {objLoop: {b: ...}}}}}
console.log('-----改變目標對象------')
copyObjLoop.b = 'bb'
console.log('源對象: ', objLoop) // 源對象: {b: {objLoop: {b: {objLoop: {b: ...}}}}}
console.log('目標對象: ', copyObjLoop) // 目標對象: {b: "bb"}

說明

實際生產環境建議用成熟的庫,好比lodash.clonedeep,此文章只爲說明JS的深拷貝和淺拷貝問題,以及怎麼實現
相關文章
相關標籤/搜索