在 JavaScript 中,有許多方法可以複製物件,但並非所有方法都提供深層複製。本文將介紹最有效的方式,並提供所有可用的選項。
2022年更新:只需使用
structuredClone()
方法進行複製。詳見如何在 JavaScript 中複製物件
在 JavaScript 中複製物件可能會很棘手。有些方法會執行淺層複製,這是大多數情況下的預設行為。
深層複製 vs 淺層複製
淺層複製能成功複製原始類型,如數字和字串,但是任何物件引用都不會被遞迴複製,而是新複製的物件將引用相同的物件。
如果物件包含其他物件的引用,在對該物件進行淺層複製時,只會複製對外部物件的引用。
而在深層複製中,這些外部物件也會被複製,因此新複製的物件與原始物件完全獨立。
當在網路上搜尋如何在 JavaScript 中深度複製物件時,你會找到很多回答,但這些回答並不總是正確的。
最簡單的選項:使用 Lodash
我建議你依賴於一個經過良好測試、非常受歡迎且精心維護的庫來執行深度複製:Lodash。
Lodash 提供了非常方便的 clone
和 cloneDeep
函數可以執行淺層和深度複製。
Lodash 還具有一個很好的特性:你可以單獨導入單個函數到你的項目中,以大大減少其依賴的大小。
在 Node.js 中:
const clone = require('lodash/clone')
const cloneDeep = require('lodash/cloneDeep')
以下是使用這兩個函數的示例:
const clone = require('lodash/clone')
const cloneDeep = require('lodash/cloneDeep')
const externalObject = {
color: 'red',
}
const original = {
a: new Date(),
b: NaN,
c: new Function(),
d: undefined,
e: function () {},
f: Number,
g: false,
h: Infinity,
i: externalObject,
}
const cloned = clone(original)
externalObject.color = 'blue'
console.info('⬇️ 淺層複製 🌈')
console.info(
'✏️ 注意,我們在原始物件上更改的 i.color 屬性在淺層複製中也發生了變化'
)
console.log(original)
console.log(cloned)
const deepCloned = cloneDeep(original)
externalObject.color = 'yellow'
console.log('')
console.info('⬇️ 深層複製 🌈')
console.info(
'✏️ 注意,i.color 屬性不再傳播'
)
console.log(original)
console.log(deepCloned)
在這個簡單的示例中,我們首先進行了淺層複製,然後編輯了 i.color
屬性,它會傳播到複製的物件中。
而在深層複製中,這種情況不會發生。
使用 Object.assign()
Object.assign()
執行的是物件的淺層複製,而非深度複製。
const copied = Object.assign({}, original)
由於它是淺層複製,它會將值複製過去,而物件引用只會被複製(而不是物件本身),所以如果你在原始物件中編輯物件屬性,複製的物件也會被修改,因為內部引用的物件是相同的:
const original = {
name: 'Fiesta',
car: {
color: 'blue',
}
}
const copied = Object.assign({}, original)
original.name = 'Focus'
original.car.color = 'yellow'
copied.name // Fiesta
copied.car.color // yellow
使用物件展開運算子
展開運算子 是一個 ES6/ES2015 功能,它提供了一種很方便的方法來執行淺層複製,相當於 Object.assign()
。
const copied = { ...original }
錯誤的解決方案
在網路上你會找到很多建議。以下是一些錯誤的解決方案:
使用 Object.create()
注意:不推薦使用
const copied = Object.create(original)
這是錯誤的,它並不會進行任何複製。
相反,original
物件被用作 copied
的 原型。
雖然它看起來可以運作,但其實不是這樣的:
const original = {
name: 'Fiesta',
}
const copied = Object.create(original)
copied.name // Fiesta
original.hasOwnProperty('name') // true
copied.hasOwnProperty('name') // false
JSON序列化
注意:不推薦使用
有些人建議先將物件轉換為 JSON 格式:
const cloned = JSON.parse(JSON.stringify(original))
但這會有一些意想不到的後果。
這樣做會丟失沒有 JSON 等效類型的 JavaScript 特性,例如Function
或Infinity
,所有值為undefined
的屬性會被JSON.stringify
忽略,從而導致它們在複製的物件中被漏掉。
同時,一些物件會被轉換為字符串,比如 Date 物件(同時,不考慮時區並默認為UTC)、Set、Map 等等:
JSON.parse(
JSON.stringify({
a: new Date(),
b: NaN,
c: new Function(),
d: undefined,
e: function () {},
f: Number,
g: false,
h: Infinity,
})
)
只有在你沒有任何內部物件和函數,僅有值的情況下,此方法才能正常運作。