在 JavaScript 中,有許多方法可以複製物件,但並非所有方法都提供深層複製。本文將介紹最有效的方式,並提供所有可用的選項。

2022年更新:只需使用structuredClone()方法進行複製。詳見如何在 JavaScript 中複製物件

在 JavaScript 中複製物件可能會很棘手。有些方法會執行淺層複製,這是大多數情況下的預設行為。

深層複製 vs 淺層複製

淺層複製能成功複製原始類型,如數字和字串,但是任何物件引用都不會被遞迴複製,而是新複製的物件將引用相同的物件。

如果物件包含其他物件的引用,在對該物件進行淺層複製時,只會複製對外部物件的引用

而在深層複製中,這些外部物件也會被複製,因此新複製的物件與原始物件完全獨立。

當在網路上搜尋如何在 JavaScript 中深度複製物件時,你會找到很多回答,但這些回答並不總是正確的。

最簡單的選項:使用 Lodash

我建議你依賴於一個經過良好測試、非常受歡迎且精心維護的庫來執行深度複製:Lodash。

Lodash 提供了非常方便的 clonecloneDeep 函數可以執行淺層和深度複製。

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

詳見Object.create()

JSON序列化

注意:不推薦使用

有些人建議先將物件轉換為 JSON 格式:

const cloned = JSON.parse(JSON.stringify(original))

但這會有一些意想不到的後果。

這樣做會丟失沒有 JSON 等效類型的 JavaScript 特性,例如FunctionInfinity,所有值為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,
 })
)

使用JSON解析

只有在你沒有任何內部物件和函數,僅有值的情況下,此方法才能正常運作。