JS淺拷貝深拷貝原理以及常用方法大全
前言
在JavaScript中,拷貝是我們?nèi)粘i_(kāi)發(fā)中高頻用到的操作,但很多小伙伴會(huì)被“淺拷貝”和“深拷貝”搞暈——為什么有時(shí)候修改原對(duì)象,新對(duì)象也跟著變?有時(shí)候卻完全不受影響?
其實(shí)答案很簡(jiǎn)單:拷貝只針對(duì)引用類(lèi)型(對(duì)象、數(shù)組等),基本類(lèi)型(數(shù)字、字符串、布爾值)不存在“深淺”一說(shuō)(賦值就是直接拷貝值)。今天我們就結(jié)合具體代碼,把淺拷貝和深拷貝講透。
先明確核心概念
無(wú)論是淺拷貝還是深拷貝,核心都是「基于原對(duì)象,創(chuàng)建一個(gè)新對(duì)象」,區(qū)別在于:拷貝的“深度”不同,是否會(huì)拷貝原對(duì)象內(nèi)部的子引用類(lèi)型。
重點(diǎn)牢記:引用類(lèi)型的賦值,賦值的是「內(nèi)存地址」,不是值本身;而拷貝是創(chuàng)建新對(duì)象,只是拷貝的“層數(shù)”有差異。
一、淺拷貝:只抄“表面”,子對(duì)象還是“共用”
淺拷貝的特點(diǎn):只拷貝原對(duì)象的第一層屬性。如果原對(duì)象的屬性值是引用類(lèi)型(比如子對(duì)象、數(shù)組),那么拷貝的不是值,而是這個(gè)子對(duì)象的內(nèi)存地址——也就是說(shuō),原對(duì)象和新對(duì)象的子引用類(lèi)型,共用同一個(gè)“空間”,修改其中一個(gè),另一個(gè)會(huì)跟著變。
1. 數(shù)組淺拷貝:slice() 方法
const arr = ['a', 'b', 'c', 'd', {age: 18}]
const arr2 = arr.slice(0) // 淺拷貝數(shù)組arr(slice不傳參、傳0效果一致,均拷貝整個(gè)數(shù)組)
arr[4].age = 20 // 修改原數(shù)組中的子對(duì)象屬性
console.log(arr2); // 輸出:['a','b','c','d',{age:20}]?? 解析:slice(0) 會(huì)創(chuàng)建一個(gè)新數(shù)組,拷貝原數(shù)組的第一層元素。但數(shù)組中第4個(gè)元素是對(duì)象(引用類(lèi)型),拷貝的是它的地址,所以修改原數(shù)組中這個(gè)對(duì)象的age,新數(shù)組arr2中的對(duì)應(yīng)對(duì)象也會(huì)同步變化。
2. 數(shù)組淺拷貝:擴(kuò)展運(yùn)算符 [...arr]
const a = [1, 2, {age: 18}]
let c = [...a] // 用擴(kuò)展運(yùn)算符淺拷貝數(shù)組a
a[2].age = 19 // 修改原數(shù)組中的子對(duì)象屬性
console.log(c); // 輸出:[1,2,{age:19}]?? 解析:擴(kuò)展運(yùn)算符是最簡(jiǎn)潔的數(shù)組淺拷貝方式之一,原理和slice()一致,只拷貝第一層。子對(duì)象依然是共用地址,修改原對(duì)象子屬性,新對(duì)象會(huì)受影響。
3. 數(shù)組淺拷貝:concat() 方法
const a = [1, 2, 3, {age: 18}]
const d = [].concat(a) // 用concat()淺拷貝數(shù)組a
a[3].age = 20 // 修改原數(shù)組中的子對(duì)象屬性
console.log(d); // 輸出:[1,2,3,{age:20}]?? 解析:concat() 原本用于拼接數(shù)組,當(dāng)只傳入一個(gè)原數(shù)組時(shí),會(huì)返回一個(gè)新數(shù)組,本質(zhì)也是淺拷貝,子引用類(lèi)型依然共用地址。
4. 數(shù)組淺拷貝:toReversed().reverse() (特殊小技巧)
const arr = [1, 2, 3] const arr2 = arr.toReversed().reverse() // 先反轉(zhuǎn)再反轉(zhuǎn),得到原數(shù)組的淺拷貝 console.log(arr2); // 輸出:[1,2,3]
?? 解析:toReversed() 會(huì)返回一個(gè)反轉(zhuǎn)后的新數(shù)組(不修改原數(shù)組),再調(diào)用reverse() 反轉(zhuǎn)回來(lái),相當(dāng)于創(chuàng)建了一個(gè)和原數(shù)組完全一樣的新數(shù)組,屬于淺拷貝。
5. 對(duì)象淺拷貝:Object.assign() 方法
const obj = {
name: '跳跳虎',
like: ['偵探'] // 子屬性是數(shù)組(引用類(lèi)型)
}
const newObj = Object.assign({}, obj) // 淺拷貝obj到新對(duì)象
obj.like[0] = '蹦蹦跳跳' // 修改原對(duì)象的子數(shù)組
console.log(newObj); // 輸出:{name: '跳跳虎', like: ['蹦蹦跳跳']}?? 解析:Object.assign(target, source) 會(huì)將source對(duì)象的屬性拷貝到target對(duì)象,返回target。這里target是空對(duì)象,相當(dāng)于創(chuàng)建了一個(gè)新對(duì)象,拷貝原對(duì)象的第一層屬性。子數(shù)組like是引用類(lèi)型,拷貝的是地址,所以修改原對(duì)象的like,newObj的like也會(huì)變。
6. 手動(dòng)實(shí)現(xiàn)淺拷貝
const obj = {
name: '跳跳虎',
age: 18,
like: {
n: '偵探',
m: '蹦蹦跳跳'
}
}
// 自定義淺拷貝函數(shù)
function shallowCopy(obj) {
let o = {} // 創(chuàng)建新對(duì)象
// 遍歷原對(duì)象,拷貝第一層屬性(只拷貝自身可枚舉屬性)
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 排除原型鏈上的屬性
o[key] = obj[key] // 引用類(lèi)型拷貝地址,基本類(lèi)型拷貝值
}
}
return o
}
const newObj = shallowCopy(obj)
obj.like.m = '麥芽糖' // 修改原對(duì)象的子對(duì)象屬性
console.log(newObj); // 輸出:like.m 也變成了'麥芽糖'?? 解析:手動(dòng)實(shí)現(xiàn)的淺拷貝邏輯很簡(jiǎn)單——創(chuàng)建新對(duì)象,遍歷原對(duì)象的自身屬性,將屬性值賦值給新對(duì)象。和內(nèi)置方法一樣,子引用類(lèi)型拷貝的是地址,會(huì)受原對(duì)象影響。
二、深拷貝:層層拷貝,完全“獨(dú)立”
深拷貝的特點(diǎn):層層拷貝,直到所有層級(jí)的屬性都是基本類(lèi)型。新對(duì)象和原對(duì)象完全獨(dú)立,修改原對(duì)象的任何屬性(包括子對(duì)象、子數(shù)組),新對(duì)象都不會(huì)有任何變化——相當(dāng)于“復(fù)制粘貼”了一整個(gè)對(duì)象,連里面的“小零件”都復(fù)制了一份。
1. 深拷貝:structuredClone() 方法
const obj = {
name: '跳跳虎',
age: 18,
like: {
n: '偵探',
m: '蹦蹦跳跳'
},
a: 123n, // BigInt類(lèi)型
date: new Date(), // Date類(lèi)型
map: new Map([['key1', 'value1']]), // Map類(lèi)型
say() {
console.log('hello');
}
}
const newObj = structuredClone(obj) // 深拷貝obj
obj.like.m = '麥芽糖' // 修改原對(duì)象的子對(duì)象屬性
obj.date.setFullYear(2025) // 修改原對(duì)象的Date屬性
console.log(newObj);
// 輸出:like.m 依然是'蹦蹦跳跳',date依然是原日期(不受影響)
// 注意:say()函數(shù)被忽略,map被正??截?/pre>?? 解析:structuredClone() 是ES2022新增的深拷貝方法,用法簡(jiǎn)單,能處理大部分引用類(lèi)型(對(duì)象、數(shù)組、Map、Set、Date、RegExp、ArrayBuffer等),支持BigInt類(lèi)型,且能正確拷貝循環(huán)引用(比如obj.self = obj,不會(huì)報(bào)錯(cuò))。
注意:structuredClone() 有以下局限:
- 無(wú)法拷貝函數(shù)(普通函數(shù)、箭頭函數(shù)、類(lèi)方法均會(huì)被忽略);
- 無(wú)法拷貝原型鏈上的屬性(只拷貝對(duì)象自身屬性);
- 無(wú)法拷貝Error、Function、GeneratorFunction、AsyncFunction等類(lèi)型;
- 無(wú)法拷貝Symbol類(lèi)型(會(huì)被忽略);
- 無(wú)法拷貝DOM節(jié)點(diǎn)(會(huì)報(bào)錯(cuò))。
2. 深拷貝:JSON.parse(JSON.stringify(obj))
const obj = {
name: '跳跳虎',
age: 18,
like: {
n: '偵探',
m: '蹦蹦跳跳'
},
say() {
console.log('hello');
},
a: undefined, // undefined類(lèi)型
b: null,
c: NaN, // NaN類(lèi)型
d: Infinity // 無(wú)窮大類(lèi)型
}
const oo = JSON.parse(JSON.stringify(obj)) // 深拷貝
obj.like.m = '麥芽糖' // 修改原對(duì)象
console.log(oo);
// 輸出:{name: '跳跳虎', age:18, like:{n:'偵探',m:'蹦蹦跳跳'}, b:null, c:null, d:null}?? 解析:這是最常用的“民間深拷貝方法”,原理是:先將對(duì)象轉(zhuǎn)為JSON字符串(JSON.stringify),再將字符串轉(zhuǎn)回對(duì)象(JSON.parse),相當(dāng)于重新創(chuàng)建了一個(gè)完全獨(dú)立的對(duì)象。
注意:這個(gè)方法有明顯局限,無(wú)法處理以下類(lèi)型,會(huì)自動(dòng)忽略或轉(zhuǎn)換:
函數(shù)(say()方法被忽略)
BigInt(會(huì)報(bào)錯(cuò),無(wú)法轉(zhuǎn)換)
undefined(被忽略)
NaN、Infinity(會(huì)轉(zhuǎn)為null)
3. 手動(dòng)實(shí)現(xiàn)深拷貝
const obj = {
name: '跳跳虎',
age: 18,
like: {
n: '偵探',
m: '蹦蹦跳跳'
}
}
// 自定義遞歸深拷貝函數(shù)
function deepClone(obj) {
let o = {} // 創(chuàng)建新對(duì)象
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 只拷貝自身屬性
// 判斷當(dāng)前屬性值是否是引用類(lèi)型(且不是null,因?yàn)閚ull的typeof也是object)
if (typeof(obj[key]) == 'object' && obj[key] !== null) {
// 遞歸調(diào)用深拷貝,對(duì)子對(duì)象也進(jìn)行深拷貝
const childObj = deepClone(obj[key])
o[key] = childObj
} else {
// 基本類(lèi)型,直接拷貝值
o[key] = obj[key]
}
}
}
return o
}
const newObj = deepClone(obj)
obj.like.m = '麥芽糖' // 修改原對(duì)象
console.log(newObj); // 輸出:like.m 依然是'蹦蹦跳跳'(完全獨(dú)立)?? 解析:手動(dòng)深拷貝的核心是「遞歸」——遍歷對(duì)象時(shí),如果遇到引用類(lèi)型(且不是null),就遞歸調(diào)用自身,對(duì)這個(gè)子對(duì)象也進(jìn)行深拷貝;如果是基本類(lèi)型,就直接賦值。這樣就能實(shí)現(xiàn)“層層拷貝”,新對(duì)象和原對(duì)象完全獨(dú)立。
補(bǔ)充:這個(gè)基礎(chǔ)版遞歸深拷貝,還可以?xún)?yōu)化(比如處理數(shù)組、Map、Set等),但日常開(kāi)發(fā)中,處理普通對(duì)象和數(shù)組已經(jīng)足夠啦~
三、補(bǔ)充知識(shí)點(diǎn):for...in 遍歷與hasOwnProperty()
在上面的拷貝函數(shù)中,我們都用到了 for (let key in obj) 和 obj.hasOwnProperty(key),很多小伙伴會(huì)疑惑這兩個(gè)的作用,這里簡(jiǎn)單補(bǔ)充:
// 給Object原型添加一個(gè)屬性(模擬原型鏈上的屬性)
Object.prototype.d = 4
const obj = {
a: 1,
b: 2,
c: 3
}
// for...in 會(huì)遍歷對(duì)象自身的屬性 + 原型鏈上的可枚舉屬性
for (let key in obj) {
// hasOwnProperty(key):判斷key是否是對(duì)象自身的屬性(排除原型鏈上的屬性)
if (obj.hasOwnProperty(key)) {
console.log(key); // 輸出:a, b, c(不會(huì)輸出d)
}
}?? 解析:拷貝時(shí),我們只需要拷貝對(duì)象「自身的屬性」,不需要拷貝原型鏈上的屬性(比如上面的d屬性),所以用hasOwnProperty() 來(lái)過(guò)濾,避免拷貝多余的屬性。
四、淺拷貝vs深拷貝 總結(jié)表
| 類(lèi)型 | 核心特點(diǎn) | 常用方法 | 注意事項(xiàng) |
|---|---|---|---|
| 淺拷貝 | 只拷貝第一層,子引用類(lèi)型共用地址 | slice()、[...arr]、concat()、Object.assign() | 修改原對(duì)象子引用類(lèi)型,新對(duì)象會(huì)受影響 |
| 深拷貝 | 層層拷貝,新對(duì)象與原對(duì)象完全獨(dú)立 | structuredClone()、JSON.parse(JSON.stringify()) | 各方法有局限(如無(wú)法拷貝函數(shù)、BigInt等) |
最后小總結(jié)
1. 基本類(lèi)型不用糾結(jié)深淺拷貝,賦值就是拷貝值;
2. 淺拷貝適合簡(jiǎn)單場(chǎng)景(原對(duì)象只有第一層基本類(lèi)型),高效簡(jiǎn)潔;
3. 深拷貝適合復(fù)雜場(chǎng)景(原對(duì)象有多層引用類(lèi)型),保證數(shù)據(jù)獨(dú)立,避免意外修改;
4. 選擇拷貝方法時(shí),根據(jù)需求選:
- 需拷貝Date、Map、循環(huán)引用 → 用structuredClone();
- 簡(jiǎn)單對(duì)象/數(shù)組,無(wú)特殊類(lèi)型 → 用JSON.parse(JSON.stringify())(注意局限);
- 有特殊類(lèi)型(如函數(shù)、Symbol)、需自定義拷貝規(guī)則 → 用完善版手動(dòng)深拷貝;
- 簡(jiǎn)單數(shù)組/對(duì)象,無(wú)需深層拷貝 → 用擴(kuò)展運(yùn)算符、Object.assign()等淺拷貝方法
結(jié)合上面的代碼實(shí)例,能輕松區(qū)分淺拷貝和深拷貝,下次遇到拷貝問(wèn)題,再也不用慌啦!
到此這篇關(guān)于JS淺拷貝深拷貝原理以及常用方法大全的文章就介紹到這了,更多相關(guān)JS淺拷貝深拷貝用法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript 防抖和節(jié)流遇見(jiàn)的奇怪問(wèn)題及解決
這篇文章主要介紹了JavaScript 防抖和節(jié)流遇見(jiàn)的奇怪問(wèn)題及解決,幫助大家更好的理解和使用JavaScript,感興趣的朋友可以了解下2020-11-11
Jquery和JS用外部變量獲取Ajax返回的參數(shù)值的方法實(shí)例(超簡(jiǎn)單)
Jquery和JS用外部變量獲取Ajax返回的參數(shù)值的方法實(shí)例(超簡(jiǎn)單),需要的朋友可以參考一下2013-06-06
教你3分鐘利用原生js實(shí)現(xiàn)有進(jìn)度監(jiān)聽(tīng)的文件上傳預(yù)覽組件
這篇文章主要給大家介紹了關(guān)于如何3分鐘利用原生js實(shí)現(xiàn)有進(jìn)度監(jiān)聽(tīng)的文件上傳預(yù)覽組件的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用js具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07
基于rollup的組件庫(kù)打包體積優(yōu)化小結(jié)
這篇文章主要介紹了基于rollup的組件庫(kù)打包體積優(yōu)化小結(jié),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06
js驗(yàn)證框架之RealyEasy驗(yàn)證詳解
這篇文章主要為大家詳細(xì)介紹了js驗(yàn)證框架之RealyEasy驗(yàn)證,記錄了RealyEasy驗(yàn)證的使用步驟,感興趣的小伙伴們可以參考一下2016-06-06
JavaScript關(guān)鍵字this的使用方法詳解
與其他語(yǔ)言相比,函數(shù)的 this 關(guān)鍵字在 JavaScript 中的表現(xiàn)略有不同,此外,在嚴(yán)格模式和非嚴(yán)格模式之間也會(huì)有一些差別,本文就給大家講解一下JavaScript關(guān)鍵字中的this,需要的朋友可以參考下2023-08-08
Layui表格行內(nèi)動(dòng)態(tài)編輯數(shù)據(jù)
本文主要介紹經(jīng)典前端框架 layui 中的動(dòng)態(tài)表格數(shù)據(jù)操作,結(jié)合 JQuery 動(dòng)態(tài)編輯單元格中的數(shù)據(jù),具有一定的參考價(jià)值,感興趣的可以了解一下2021-08-08

