前端面試手寫練習 - cloneDeep
- 文章發表於
問題
深拷貝 (Deep Clone) 是將其理解為將一個物件及其所有屬性和子屬性完整複製到一個新物件中。在這個過程中,複製的值不會指向原始物件的屬性,因此對深拷貝後的物件進行的任何更改都不會影響原始物件。
接者我們將實現一個 cloneDeep
函式,這個函式會對 JavaScript 的物件進行深拷貝。
function cloneDeep(obj: any): any
範例
const taipei1 = { city: { population: 2600000 } }const clonedTaipei1 = deepClone(taipei1)clonedTaipei1.city.population = 2700000 // 改變複製後的人口數console.log(clonedTaipei1.city.population) // 2700000console.log(taipei1.city.population) // 2600000const taipei2 = { landmarks: [{ name: '台北 101' }] }const clonedTaipei2 = deepClone(taipei2)taipei2.landmarks[0].name = '大安森林公園' // 修改原始物件中的地標名稱console.log(taipei2.landmarks[0].name) // '大安森林公園'console.log(clonedTaipei2.landmarks[0].name) // '台北 101'
練習區
在了解問題後,可以嘗試先寫下您的思路,再到下方的練習區域實際寫出程式碼。
追問
可以將測試案例中的
skip
移除,並實作以下追問:
- 是否能處理循環引用、Symbol 屬性與陣列的深拷貝?
筆者思路
簡易版
- 如果
value
不是物件或是null
,直接返回value
。 - 如果
value
是物件,創建一個新的物件result
。 - 使用
Object.getOwnPropertyNames
取得value
的所有屬性,並遍歷其所有屬性,對每個屬性進行深拷貝,並將其放入result
中。 - 返回
result
。
追問
- 對於陣列的深拷貝,可以新增一個條件是專門檢查
value
是否是陣列,如果是陣列的話,我們就對陣列中的每個元素進行深拷貝。 - 對於循環引用的處理,可以使用
Map
來記錄已經複製過的物件,當我們遇到循環引用時,我們可以直接返回Map
中的物件。 - 對於 Symbol 屬性的處理,可以直接在
for...in
迴圈中加入Object.getOwnPropertySymbols
來處理。
筆者解答
解法一. 使用 JSON.parse 和 JSON.stringify
程式碼
function cloneDeep(value) {return JSON.parse(JSON.stringify(value))}
詳解
在 strucutredClone
API 還沒出來以前,要快速對一個物件進行深拷貝的其中一個選擇就是用 JSON.parse
和 JSON.stringify
。
但這個方法有一些限制,例如:
const target = {date: new Date(),name: 'John Doe',}const cloned = cloneDeep(target)console.log(cloned.date instanceof Date) // ❌ false, 會將其轉成字串
因為 JSON.stringify
只能處理基本物件、陣列以及字串,所以當我們對 Date
物件進行深拷貝時,它會被轉換成字串,另外像是 Map
、Set
、Function
等等都無法正確的被深拷貝,也無法處理循環引用的情況。
strucutredClone
API
解法二. 使用 程式碼
function cloneDeep(value) {return structuredClone(value)}
詳解
structuredClone
API 是 2022 年才開始被各大瀏覽器支援的方法 - 可以參考 Web Status Platform,它可以用來創建複雜 JavaScript 物件的深拷貝。
雖然說它的能力比使用 JSON.parse
和 JSON.stringify
強大,但是它也有一些限制,例如:
無法處理 Symbol properties (符號屬性):
const sym = Symbol('symbol')const obj = { [sym]: 'John Doe' }const clonedObj = structuredClone(obj)console.log(clonedObj[sym]) // ❌ undefined無法處理 Function properties (函式屬性):
const obj = {func: function () {return 'Hello'},}const clonedObj = structuredClone(obj)console.log(clonedObj.func) // ❌ undefined
除了這些還有像是無法複製 DOM 節點、物件原型鏈等,如果想要看更多 structuredClone
的使用與其限制,可以參考 MDN - structuredClone。
解法三. 手寫深拷貝
基本
function cloneDeep(value) {if (typeof value !== 'object' || value === null) {return value;}let result = {}[...Object.getOwnPropertyNames(value)].forEach((key) => {result[key] = cloneDeep(value[key]);});return result;}export { cloneDeep };
追問
function cloneDeep(value, cloned = new Map()) {if (typeof value !== 'object' || value === null) {return value;}if (Array.isArray(value)) {return value.map((item) => cloneDeep(item, cloned));}if (cloned.has(value)) {return cloned.get(value);}let result = {};cloned.set(value, result)[...Object.getOwnPropertyNames(value), ...Object.getOwnPropertySymbols(value)].forEach((key) => {result[key] = cloneDeep(value[key], cloned);});return result;}export { cloneDeep };