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