在本章中,我們將學習如何複製 JavaScript 中的物件和陣列。
資料可以有兩種「深度」的複製方式
下一節將介紹兩種複製方式。很遺憾的是,JavaScript 僅內建支援淺層複製。如果我們需要深度複製,就必須自己實作。
讓我們來看看幾種淺層複製資料的方法。
唉,擴散有幾個問題。這些問題將在下一小節中討論。其中,有些是真正的限制,有些只是特殊情況。
例如
class MyClass {}
const original = new MyClass();
assert.equal(original instanceof MyClass, true);
const copy = {...original};
assert.equal(copy instanceof MyClass, false);
請注意,以下兩個表達式是等效的
因此,我們可以透過將副本賦予與原始物件相同的原型來修正這個問題
class MyClass {}
const original = new MyClass();
const copy = {
__proto__: Object.getPrototypeOf(original),
...original,
};
assert.equal(copy instanceof MyClass, true);
或者,我們可以在建立副本後透過 Object.setPrototypeOf()
來設定副本的原型。
此類內建物件的範例包括正規表示式和日期。如果我們複製它們,我們會遺失儲存在它們中的大部分資料。
考量到 原型鏈 的運作方式,這通常是正確的做法。但我們仍需要意識到這一點。在以下範例中,original
的繼承屬性 .inheritedProp
在 copy
中不可用,因為我們只複製自有屬性,而且不保留原型。
const proto = { inheritedProp: 'a' };
const original = {__proto__: proto, ownProp: 'b' };
assert.equal(original.inheritedProp, 'a');
assert.equal(original.ownProp, 'b');
const copy = {...original};
assert.equal(copy.inheritedProp, undefined);
assert.equal(copy.ownProp, 'b');
例如,陣列實例的自有屬性 .length
不可列舉,而且不會被複製。在以下範例中,我們透過物件擴散複製陣列 arr
(A 行)
const arr = ['a', 'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true);
const copy = {...arr}; // (A)
assert.equal({}.hasOwnProperty.call(copy, 'length'), false);
這也極少會造成限制,因為大部分屬性都是可列舉的。如果我們需要複製不可列舉的屬性,我們可以使用 Object.getOwnPropertyDescriptors()
和 Object.defineProperties()
來複製物件(稍後會說明如何執行此操作)
value
),因此會正確複製 getter、setter、唯讀屬性等。Object.getOwnPropertyDescriptors()
會擷取可列舉和不可列舉的屬性。如需有關可列舉性的詳細資訊,請參閱 §12「屬性的可列舉性」。
與 屬性的屬性 無關,其副本永遠會是可寫入且可設定的資料屬性。
例如,在此我們建立屬性 original.prop
,其屬性 writable
和 configurable
為 false
const original = Object.defineProperties(
{}, {
prop: {
value: 1,
writable: false,
configurable: false,
enumerable: true,
},
});
assert.deepEqual(original, {prop: 1});
如果我們複製 .prop
,則 writable
和 configurable
都是 true
const copy = {...original};
// Attributes `writable` and `configurable` of copy are different:
assert.deepEqual(
Object.getOwnPropertyDescriptors(copy),
{
prop: {
value: 1,
writable: true,
configurable: true,
enumerable: true,
},
});
因此,getter 和 setter 也無法忠實地複製
const original = {
get myGetter() { return 123 },
set mySetter(x) {},
};
assert.deepEqual({...original}, {
myGetter: 123, // not a getter anymore!
mySetter: undefined,
});
前面提到的 Object.getOwnPropertyDescriptors()
和 Object.defineProperties()
始終會完整地傳輸具有所有屬性的自有屬性(如下所示)。
副本具有原件中每個鍵值條目的最新版本,但原件的值本身並未複製。例如
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = {...original};
// Property .name is a copy: changing the copy
// does not affect the original
copy.name = 'John';
assert.deepEqual(original,
{name: 'Jane', work: {employer: 'Acme'}});
assert.deepEqual(copy,
{name: 'John', work: {employer: 'Acme'}});
// The value of .work is shared: changing the copy
// affects the original
copy.work.employer = 'Spectre';
assert.deepEqual(
original, {name: 'Jane', work: {employer: 'Spectre'}});
assert.deepEqual(
copy, {name: 'John', work: {employer: 'Spectre'}});
我們將在本章後續部分探討深度複製。
Object.assign()
進行淺層複製(選用)Object.assign()
的運作方式大致上與擴散到物件中相同。也就是說,下列兩種複製方式大致上是等效的
使用函式而非語法的好處是,它可以在較舊的 JavaScript 引擎上透過函式庫進行多型填充。
不過,Object.assign()
並不完全像擴散。它有一個相對細微的差異:它以不同的方式建立屬性。
Object.assign()
使用指定來建立副本的屬性。在其他事項中,指定會呼叫自有和繼承的 setter,而定義則不會(有關指定與定義的更多資訊)。這種差異很少會被注意到。下列程式碼是一個範例,但它是人為的
const original = {['__proto__']: null}; // (A)
const copy1 = {...original};
// copy1 has the own property '__proto__'
assert.deepEqual(
Object.keys(copy1), ['__proto__']);
const copy2 = Object.assign({}, original);
// copy2 has the prototype null
assert.equal(Object.getPrototypeOf(copy2), null);
透過在 A 行中使用已計算的屬性鍵,我們建立 .__proto__
作為自有屬性,並且不會呼叫繼承的 setter。不過,當 Object.assign()
複製該屬性時,它會呼叫 setter。(有關 .__proto__
的更多資訊,請參閱 “JavaScript for impatient programmers”。)
Object.getOwnPropertyDescriptors()
和 Object.defineProperties()
進行淺層複製(選用)JavaScript 讓我們透過 屬性描述符 來建立屬性,屬性描述符是指定屬性屬性的物件。例如,透過我們已經看過的 Object.defineProperties()
。如果我們將該函式與 Object.getOwnPropertyDescriptors()
結合使用,我們可以更忠實地進行複製
function copyAllOwnProperties(original) {
return Object.defineProperties(
{}, Object.getOwnPropertyDescriptors(original));
}
這消除了透過擴散複製物件的兩個問題。
首先,自有屬性的所有屬性都正確地複製了。因此,我們現在可以複製自有 getter 和自有 setter
const original = {
get myGetter() { return 123 },
set mySetter(x) {},
};
assert.deepEqual(copyAllOwnProperties(original), original);
其次,感謝 Object.getOwnPropertyDescriptors()
,不可列舉的屬性也會被複製
const arr = ['a', 'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true);
const copy = copyAllOwnProperties(arr);
assert.equal({}.hasOwnProperty.call(copy, 'length'), true);
現在是處理深度複製的時候了。首先,我們將手動進行深度複製,然後我們將探討一般性方法。
如果我們巢狀展開,我們會得到深度複製
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = {name: original.name, work: {...original.work}};
// We copied successfully:
assert.deepEqual(original, copy);
// The copy is deep:
assert.ok(original.work !== copy.work);
這是一個技巧,但它在緊急情況下提供了一個快速解決方案:為了深度複製一個物件 original
,我們首先將它轉換成一個 JSON 字串並解析該 JSON 字串
function jsonDeepCopy(original) {
return JSON.parse(JSON.stringify(original));
}
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = jsonDeepCopy(original);
assert.deepEqual(original, copy);
這種方法的重大缺點是我們只能複製具有 JSON 支援的鍵和值的屬性。
有些不受支援的鍵和值會被忽略
assert.deepEqual(
jsonDeepCopy({
// Symbols are not supported as keys
[Symbol('a')]: 'abc',
// Unsupported value
b: function () {},
// Unsupported value
c: undefined,
}),
{} // empty object
);
其他則會導致例外
assert.throws(
() => jsonDeepCopy({a: 123n}),
/^TypeError: Do not know how to serialize a BigInt$/);
下列函式通用深度複製一個值 original
function deepCopy(original) {
if (Array.isArray(original)) {
const copy = [];
for (const [index, value] of original.entries()) {
copy[index] = deepCopy(value);
}
return copy;
} else if (typeof original === 'object' && original !== null) {
const copy = {};
for (const [key, value] of Object.entries(original)) {
copy[key] = deepCopy(value);
}
return copy;
} else {
// Primitive value: atomic, no need to copy
return original;
}
}
該函式處理三種情況
original
是陣列,我們會建立一個新陣列並將 original
的元素深度複製到其中。original
是物件,我們會使用類似的做法。original
是基本值,我們不需要做任何事。讓我們試試 deepCopy()
const original = {a: 1, b: {c: 2, d: {e: 3}}};
const copy = deepCopy(original);
// Are copy and original deeply equal?
assert.deepEqual(copy, original);
// Did we really copy all levels
// (equal content, but different objects)?
assert.ok(copy !== original);
assert.ok(copy.b !== original.b);
assert.ok(copy.b.d !== original.b.d);
請注意,deepCopy()
只修正了展開的一個問題:淺層複製。所有其他問題仍然存在:原型不會被複製,特殊物件只會被部分複製,不可列舉的屬性會被忽略,大多數屬性特徵會被忽略。
一般來說,不可能完全通用地實作複製:並非所有資料都是樹狀結構,有時我們不想複製所有屬性,等等。
deepCopy()
的更簡潔版本如果我們使用 .map()
和 Object.fromEntries()
,我們可以讓 deepCopy()
的先前實作更簡潔
function deepCopy(original) {
if (Array.isArray(original)) {
return original.map(elem => deepCopy(elem));
} else if (typeof original === 'object' && original !== null) {
return Object.fromEntries(
Object.entries(original)
.map(([k, v]) => [k, deepCopy(v)]));
} else {
// Primitive value: atomic, no need to copy
return original;
}
}
.clone()
與複製建構函式」說明了基於類別的複製模式。