【每日一面】實(shí)現(xiàn)一個(gè)深拷貝函數(shù)
基礎(chǔ)問(wèn)答
問(wèn):知道淺拷貝和深拷貝嗎?為什么要用深拷貝?
答:拷貝,可以認(rèn)為是賦值,對(duì)于 JavaScript 中的基礎(chǔ)類(lèi)型,如 string, number, null, boolean, undefined, symbol 等,在賦值給一個(gè)變量的時(shí)候,是直接拷貝值給變量,而對(duì)于引用類(lèi)型,如 object, array, function 等,則會(huì)拷貝其引用(地址)。
使用深拷貝,是為了避免操作公共對(duì)象的時(shí)候,影響到其他使用該對(duì)象的組件。
擴(kuò)展延伸
一個(gè)拷貝函數(shù),可以直接評(píng)估出來(lái)你對(duì) JavaScript 基礎(chǔ)能力掌握水平。
在理解淺拷貝和深拷貝前,需先明確拷貝的本質(zhì)。在 JavaScript 中數(shù)據(jù)類(lèi)型分為基本類(lèi)型(string、number、boolean、null、undefined、symbol、bigint)和引用類(lèi)型(object、array、function 等),這兩種類(lèi)型在內(nèi)存中存儲(chǔ)方式是不一樣的:
- 基本類(lèi)型:值直接存儲(chǔ)在棧內(nèi)存中,賦值時(shí)直接拷貝值。
- 引用類(lèi)型:值存儲(chǔ)在堆內(nèi)存中,棧內(nèi)存僅存儲(chǔ)指向堆內(nèi)存的引用地址,賦值時(shí)僅拷貝引用地址(而非實(shí)際值)。
所以,根據(jù)這兩種存儲(chǔ)方式很容易想到,淺拷貝和深拷貝的區(qū)別就在于 是否遞歸復(fù)制嵌套的引用類(lèi)型。 這里給出一個(gè)簡(jiǎn)單的定義:
- 淺拷貝(Shallow Copy):僅復(fù)制對(duì)象的表層屬性,若屬性值為引用類(lèi)型(如嵌套對(duì)象、數(shù)組),則拷貝的是引用地址(引用地址就是表層屬性),新舊對(duì)象共享嵌套數(shù)據(jù)。
- 深拷貝(Deep Copy):遞歸復(fù)制對(duì)象的所有屬性,包括嵌套的引用類(lèi)型,新舊對(duì)象完全獨(dú)立,修改拷貝后的對(duì)象不會(huì)影響原始對(duì)象的數(shù)據(jù)。
實(shí)現(xiàn)方式
淺拷貝
淺拷貝適用于無(wú)嵌套引用類(lèi)型或無(wú)需獨(dú)立嵌套數(shù)據(jù)的場(chǎng)景,實(shí)現(xiàn)方式簡(jiǎn)單,性能開(kāi)銷(xiāo)小。
- 淺拷貝對(duì)象
Object.assign()
Object.assign(target, ...sources)方法將源對(duì)象的可枚舉屬性復(fù)制到目標(biāo)對(duì)象,最后返回的是目標(biāo)對(duì)象,使用這個(gè)方法時(shí)要注意:該方法僅拷貝對(duì)象自身屬性(不包含繼承屬性),嵌套的對(duì)象僅拷貝引用,示例如下:
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);
// 測(cè)試基本類(lèi)型屬性:修改不影響原對(duì)象
shallowCopy.a = 100;
console.log(obj.a); // 輸出:1(原對(duì)象不變)
// 測(cè)試嵌套對(duì)象:修改會(huì)影響原對(duì)象
shallowCopy.b.c = 200;
console.log(obj.b.c); // 輸出:200(原對(duì)象被修改)
- 淺拷貝數(shù)組
Array.prototype.slice()和Array.prototype.concat()
這兩個(gè)方法返回的都是新數(shù)組(不在原數(shù)組上操作),示例如下:
const arr = [1, [2, 3]];
const shallowCopy1 = arr.slice(0); // 方法1:slice
const shallowCopy2 = [].concat(arr); // 方法2:concat
// 測(cè)試基本類(lèi)型元素:修改不影響原數(shù)組
shallowCopy1[0] = 100;
console.log(arr[0]); // 輸出:1(原數(shù)組不變)
// 測(cè)試嵌套數(shù)組:修改會(huì)影響原數(shù)組
shallowCopy2[1][0] = 200;
console.log(arr[1][0]); // 輸出:200(原數(shù)組被修改)
- 擴(kuò)展運(yùn)算符
...
這個(gè)是 es6 新增的運(yùn)算符,可以用于對(duì)象和數(shù)組的淺拷貝,語(yǔ)法相較于上面兩種方式比較簡(jiǎn)單,示例如下:
// 對(duì)象淺拷貝
const obj = { a: 1, b: { c: 2 } };
const shallowObj = { ...obj };
// 數(shù)組淺拷貝
const arr = [1, [2, 3]];
const shallowArr = [...arr];
深拷貝
深拷貝適用于包含嵌套引用類(lèi)型且需要完全獨(dú)立副本的場(chǎng)景,實(shí)現(xiàn)復(fù)雜度較高,需處理遞歸、循環(huán)引用等邊界情況。屬于前端八股面試必須準(zhǔn)備的一個(gè)問(wèn)題。
- 序列化方式拷貝
JSON.parse(JSON.stringify())
利用 JSON 序列化與反序列化實(shí)現(xiàn)深拷貝,語(yǔ)法簡(jiǎn)單,多數(shù)時(shí)候夠用。
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const deepCopy = JSON.parse(JSON.stringify(obj));
// 測(cè)試嵌套對(duì)象:修改不影響原對(duì)象
deepCopy.b.c = 200;
console.log(obj.b.c); // 輸出:2(原對(duì)象不變)
但是這個(gè)方式有一定的局限性:
- 不能拷貝函數(shù)(JSON不支持)
- 不能拷貝
undefined,Symbol類(lèi)型 - 不能處理循環(huán)引用
- 不支持
BigInt類(lèi)型 - 對(duì)于日期對(duì)象和正則對(duì)象,有特殊處理,解析后可能得不到我們想要的結(jié)果
- 自定義實(shí)現(xiàn)拷貝函數(shù)
思路:遍歷對(duì)象,每一次遍歷過(guò)程中判斷是否是引用類(lèi)型(對(duì)象或數(shù)組),如果是,則遞歸的調(diào)用拷貝函數(shù),若不是,則直接賦值進(jìn)行下一步。
function deepCopy(target) {
// 基本類(lèi)型直接返回
if (target === null || typeof target !== 'object') {
return target;
}
// 區(qū)分?jǐn)?shù)組和對(duì)象
let copy;
if (Array.isArray(target)) {
copy = [];
} else {
copy = {};
}
// 遍歷屬性并遞歸拷貝
for (const key in target) {
if (target.hasOwnProperty(key)) {
// 遞歸處理引用類(lèi)型
copy[key] = deepCopy(target[key]);
}
}
return copy;
}
// 測(cè)試
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const copyObj = deepCopy(obj);
copyObj.b.c = 200;
console.log(obj, copyObj, obj === copyObj, obj.b.c); // 對(duì)比輸出結(jié)果,可以發(fā)現(xiàn)兩個(gè)對(duì)象是不同的
copyObj.d[0] = 300;
console.log(obj, copyObj, obj === copyObj, obj.d[0]); // 同上
但是這個(gè)沒(méi)有處理邊界情況,主要是兩種情況:
-
循環(huán)應(yīng)用
循環(huán)引用指對(duì)象引用自身(如obj.self = obj),直接遞歸會(huì)導(dǎo)致無(wú)限循環(huán)棧溢出。可以用WeakMap存儲(chǔ)已拷貝的對(duì)象,避免在遞歸過(guò)程中重復(fù)拷貝。 -
特殊對(duì)象
類(lèi)似于 Date,RegExp 的對(duì)象,需要我們手動(dòng)特殊處理(根據(jù)類(lèi)型直接 new)
完整的深拷貝示例:
function deepCopy(target, hash = new WeakMap()) {
// 基本類(lèi)型直接返回
if (target === null || typeof target !== 'object') {
return target;
}
// 處理循環(huán)引用:若已拷貝過(guò),直接返回緩存的副本
if (hash.has(target)) {
return hash.get(target);
}
let copy;
// 處理Date
if (target instanceof Date) {
copy = new Date(target);
hash.set(target, copy);
return copy;
}
// 處理RegExp
if (target instanceof RegExp) {
copy = new RegExp(target.source, target.flags);
copy.lastIndex = target.lastIndex; // 保留lastIndex屬性
hash.set(target, copy);
return copy;
}
// 處理數(shù)組和對(duì)象
if (Array.isArray(target)) {
copy = [];
} else {
// 處理普通對(duì)象(包括自定義對(duì)象)
copy = new target.constructor(); // 保持原型鏈
}
// 緩存已拷貝的對(duì)象,解決循環(huán)引用
hash.set(target, copy);
// 遍歷屬性并遞歸拷貝
// 處理Map
if (target instanceof Map) {
target.forEach((value, key) => {
copy.set(key, deepCopy(value, hash));
});
return copy;
}
// 處理Set
if (target instanceof Set) {
target.forEach(value => {
copy.add(deepCopy(value, hash));
});
return copy;
}
// 處理普通對(duì)象和數(shù)組的屬性
for (const key in target) {
if (target.hasOwnProperty(key)) {
copy[key] = deepCopy(target[key], hash);
}
}
return copy;
}
// 測(cè)試循環(huán)引用
const obj = { name: 'test' };
obj.self = obj; // 循環(huán)引用
const copyObj = deepCopy(obj);
console.log(copyObj.self === copyObj, copyObj === obj, obj, copyObj);
// 測(cè)試特殊對(duì)象
const date = new Date();
const copyDate = deepCopy(date);
console.log(copyDate instanceof Date, copyDate === date, date, copyDate);
const reg = /abc/gim;
reg.lastIndex = 10;
const copyReg = deepCopy(reg);
console.log(copyReg, reg);
差異對(duì)比
這里我簡(jiǎn)單總結(jié)一個(gè)表來(lái)讓你快速理解二者異同:
| 對(duì)比方向 | 淺拷貝 | 深拷貝 |
|---|---|---|
| 拷貝層級(jí) | 僅拷貝對(duì)象表層屬性 | 遞歸拷貝所有層級(jí)(包括嵌套的引用類(lèi)型) |
| 內(nèi)存占用 | 較小(共享嵌套對(duì)象的內(nèi)存) | 較大(完全復(fù)制所有數(shù)據(jù),獨(dú)立占用內(nèi)存) |
| 性能開(kāi)銷(xiāo) | 低(無(wú)需遞歸,操作簡(jiǎn)單) | 高(遞歸處理,需處理邊界情況) |
| 拷貝前后對(duì)象的獨(dú)立性 | 表層屬性獨(dú)立,嵌套引用類(lèi)型共享 | 完全獨(dú)立,新舊對(duì)象無(wú)任何關(guān)聯(lián) |
| 適用場(chǎng)景 | 無(wú)嵌套引用類(lèi)型、性能優(yōu)先、無(wú)需獨(dú)立嵌套數(shù)據(jù)的情況,簡(jiǎn)單來(lái)說(shuō),不需要前后獨(dú)立的,都可以直接用淺拷貝 | 有嵌套引用類(lèi)型、需完全隔離數(shù)據(jù)、修改不能相互影響的情況 |
| 實(shí)現(xiàn)復(fù)雜度 | 簡(jiǎn)單(可通過(guò)原生方法或簡(jiǎn)單遍歷實(shí)現(xiàn)) | 復(fù)雜(需處理遞歸、循環(huán)引用、特殊對(duì)象類(lèi)型) |
面試追問(wèn)
-
直接使用
=賦值算淺拷貝還是深拷貝?
都不是,賦值運(yùn)算符只是將一個(gè)值或者引用賦給一個(gè)變量,對(duì)于基本類(lèi)型,賦值運(yùn)算符是直接復(fù)制這個(gè)值給變量,對(duì)于引用類(lèi)型,賦值運(yùn)算符則是復(fù)制引用給變量,而非對(duì)象本身。
這個(gè)和淺拷貝的定義略有差異。 -
實(shí)現(xiàn)一個(gè)淺拷貝函數(shù)?
思路就是,直接遍歷淺層對(duì)象(第一層),賦給新的對(duì)象。
function shallowCopy(target) {
// 區(qū)分目標(biāo)是數(shù)組還是對(duì)象
if (Array.isArray(target)) {
const copy = [];
for (let i = 0; i < target.length; i++) {
copy[i] = target[i];
}
return copy;
} else if (target !== null && typeof target === 'object') {
const copy = {};
// 僅拷貝自身可枚舉屬性
for (const key in target) {
if (target.hasOwnProperty(key)) {
copy[key] = target[key];
}
}
return copy;
} else {
// 基本類(lèi)型直接返回(無(wú)需拷貝)
return target;
}
}
// 測(cè)試
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const copyObj = shallowCopy(obj);
copyObj.b.c = 200;
console.log(obj.b.c); // 輸出:200(嵌套對(duì)象共享引用)
-
深拷貝的時(shí)候,怎么特殊處理函數(shù)類(lèi)型?
函數(shù)屬于引用類(lèi)型,通常不需要深拷貝,因?yàn)楹瘮?shù)體是改不了的,通常直接復(fù)制引用就行了。
如果面試時(shí)強(qiáng)烈要求你深拷貝,可以直接使用toString()+eval實(shí)現(xiàn),但可能隨之而來(lái)的會(huì)將話題轉(zhuǎn)到eval上來(lái)問(wèn)詞法作用域、嚴(yán)格模式、安全問(wèn)題等等,一般是來(lái)轉(zhuǎn)換個(gè)話題。 -
實(shí)際開(kāi)發(fā)的時(shí)候,有經(jīng)常用這兩種模式嗎?舉個(gè)場(chǎng)景說(shuō)明一下
- 前端分頁(yè),displayData 通常是直接通過(guò) slice 獲取原始列表的一部分?jǐn)?shù)據(jù),由于不需要操作,所以也不需要深拷貝
- 接口傳參,有時(shí)候我們?yōu)榱朔奖悖瑫?huì)在請(qǐng)求數(shù)據(jù)信息之后,直接將這個(gè)返回的對(duì)象賦值給某個(gè)地方,之后再提交的時(shí)候,由于接口要求的信息不同,我們有可能會(huì)直接操作這個(gè)返回對(duì)象,導(dǎo)致使用返回對(duì)象的地方出現(xiàn)變化,這種情況就需要深拷貝。


浙公網(wǎng)安備 33010602011771號(hào)