展開語法是在 JavaScript 中複製物件的常用技術,你可以:
- 展開一個數組字面量,從而複製一個數組
- 展開一個物件字面量,從而複製一個物件
但是展開語法有一個顯著的缺點 —— 它的複製是一種淺複製,也就是說,只有頂層會被複制,但物件型別的屬性是共享的。
structuredClone()
是新推出的一個功能,很快就會得到大多數瀏覽器、Node.js 和 Deno 的支援。這個 api 可以建立物件的深層副本,也就是實現深複製。本文會解釋這個 api 是如何使用的。
本文的目錄如下:
- 在哪些 JavaScript 環境中可以使用 structuredClone()?
- 透過展開語法複製物件是一種淺複製
- 透過 structuredClone() 深複製物件
- StructuredClone() 可以複製哪些值?
在哪些 JavaScript 環境中可以使用 structuredClone()
?
儘管structuredClone()
不是 ECMAScript 的一部分,但它已被新增到許多 JavaScript 環境中,並且仍然廣泛可用(現在或不久的將來):
注意:
structuredClone()
在 WebWorkers 中並不總是可用 – 檢視MDN 瀏覽器相容性表以獲取更多資訊。- 在不支援的平臺上
structuredClone
,我們可以使用polyfill。
透過展開語法複製物件是一種淺複製
在 JavaScript 中複製陣列和普通物件的一種常見方法是使用展開語法。比如下面這段程式碼:
const obj = {id: 'e1fd960b', values: ['a', 'b']};
const clone1 = {...obj};
但這種方式是一種淺複製。一方面,clone1.id
只是一個副本,因此更改它不會更改obj
:
clone1.id = 'yes';
assert.equal(obj.id, 'e1fd960b');
另一方面,clone1.values
中的陣列與 obj
共享,如果我們改變它,我們也會改變obj
:
clone1.values.push('x');
assert.deepEqual(
clone1, {id: 'yes', values: ['a', 'b', 'x']}
);
assert.deepEqual(
obj, {id: 'e1fd960b', values: ['a', 'b', 'x']}
);
透過 structuredClone()
深複製物件
結構化克隆具有以下型別簽名:
structuredClone(value: any): any
這個函式有第二個引數,但它很少有用,超出了本文的範圍,具體的你可以參閱 MDN 頁面structuredClone()
。
structuredClone()
可以深複製物件:
const obj = {id: 'e1fd960b', values: ['a', 'b']};
const clone2 = structuredClone(obj);
clone2.values.push('x');
assert.deepEqual(
clone2, {id: 'e1fd960b', values: ['a', 'b', 'x']}
);
assert.deepEqual(
obj, {id: 'e1fd960b', values: ['a', 'b']}
);
哪些值可以使用structuredClone()
複製?
大多數內建值都可以複製
可以複製原始值:
> typeof structuredClone(true)
'boolean'
> typeof structuredClone(123)
'number'
> typeof structuredClone('abc')
'string'
大多數內建物件都可以複製 —— 即使它們有內部插槽:
> Array.isArray(structuredClone([]))
true
> structuredClone(/^a+$/) instanceof RegExp
true
但是,在複製正則表示式時,屬性.lastIndex
始終重置為零。
某些內建值無法複製
一些內建物件不能被複制 —— 如果我們嘗試複製下面的物件,structuredClone()
會丟擲一個DOMException
:
- 函式(普通函式、箭頭函式、類、方法)
- DOM 節點
前者的示範:
assert.throws(
() => structuredClone(function () {}), // ordinary function
DOMException
);
assert.throws(
() => structuredClone(() => {}), // arrow function
DOMException
);
assert.throws(
() => structuredClone(class {}),
DOMException
);
const objWithMethod = {
myMethod() {},
};
assert.throws(
() => structuredClone(objWithMethod.myMethod), // method
DOMException
);
assert.throws(
() => structuredClone(objWithMethod), // object with method
DOMException
);
structuredClone()
丟擲的異常是什麼樣的?
try {
structuredClone(() => {});
} catch (err) {
assert.equal(
err instanceof DOMException, true
);
assert.equal(
err.name, 'DataCloneError'
);
assert.equal(
err.code, DOMException.DATA_CLONE_ERR
);
}
使用者定義類的例項變成普通物件
在下面的示例中,我們複製了 class 的一個例項C
,但是,clone
不是C
的例項。
class C {}
const clone = structuredClone(new C());
assert.equal(clone instanceof C, false);
assert.equal(
Object.getPrototypeOf(clone),
Object.prototype
);
總結一下 —— structuredClone()
永遠不會複製物件的原型鏈:
- 內建物件的副本具有與原始物件相同的原型。
- 使用者定義類的例項副本始終具有原型
Object.prototype
(如普通物件)。
複製物件的屬性
structuredClone()
並不總是會複製物件的屬性:
- 訪問器變成了資料屬性
- 在副本中,屬性始終具有預設值
訪問器成為資料屬性
訪問器成為資料屬性:
const obj = Object.defineProperties(
{},
{
accessor: {
get: function () {
return 123;
},
set: undefined,
enumerable: true,
configurable: true,
},
}
);
const copy = structuredClone(obj);
assert.deepEqual(
Object.getOwnPropertyDescriptors(copy),
{
accessor: {
value: 123,
writable: true,
enumerable: true,
configurable: true,
},
}
);
屬性的副本具有預設屬性值
副本的資料屬性始終具有以下屬性:
writable: true,
enumerable: true,
configurable: true,
const obj = Object.defineProperties(
{},
{
accessor: {
get: function () {
return 123;
},
set: undefined,
enumerable: true,
configurable: true,
},
readOnlyProp: {
value: 'abc',
writable: false,
enumerable: true,
configurable: true,
},
}
);
const copy = structuredClone(obj);
assert.deepEqual(
Object.getOwnPropertyDescriptors(copy),
{
accessor: {
value: 123,
writable: true,
enumerable: true,
configurable: true,
},
readOnlyProp: {
value: 'abc',
writable: true,
enumerable: true,
configurable: true,
}
}
);