一次徹底搞懂JavaScript中的引用賦值、淺拷貝和深拷貝
前言
如果你經(jīng)常搞混 深淺拷貝 和 引用賦值,總是記不住它們有什么區(qū)別,在實(shí)際開發(fā)中總是踩坑——比如不小心修改了原始數(shù)據(jù)、或者拷貝不徹底導(dǎo)致奇怪的 bug——那么恭喜你,這篇文章就是為你寫的!我會用最直白的語言、清晰的圖示和大量實(shí)際代碼示例,幫你一次性徹底搞懂!在深入探討拷貝機(jī)制之前,我們需要先了解 JavaScript 的數(shù)據(jù)類型分類和內(nèi)存存儲機(jī)制的基礎(chǔ)概念
一、基礎(chǔ)概念鋪墊
1. JavaScript 數(shù)據(jù)類型分類
- 基本數(shù)據(jù)類型:字符串(String)、數(shù)字(Number)、布爾(Boolean)、空(Null)、未定義(Undefined)、Symbol、BigInt。
- 引用數(shù)據(jù)類型:對象(Object)、數(shù)組(Array)、函數(shù)(Function),還有兩個特殊的對象:正則(RegExp)、日期(Date)、正則表達(dá)式、Map、Set、和其他內(nèi)置對象(比如Promise、Error等)
2. 內(nèi)存存儲機(jī)制核心原理
JS 引擎將內(nèi)存劃分為棧內(nèi)存(Stack) 和堆內(nèi)存(Heap),不同類型的數(shù)據(jù)會被分配到不同的內(nèi)存區(qū)域:嵌套引用依舊遵循下列規(guī)則
| 特性 | 棧內(nèi)存 (Stack) | 堆內(nèi)存 (Heap) |
|---|---|---|
| 存儲內(nèi)容 | 基本數(shù)據(jù)類型值、引用類型的指針 | 引用數(shù)據(jù)類型的實(shí)際內(nèi)容 |
| 數(shù)據(jù)結(jié)構(gòu) | 后進(jìn)先出 (LIFO) | 動態(tài)分配的樹/圖結(jié)構(gòu) |
| 分配方式 | 連續(xù)內(nèi)存,自動分配 | 隨機(jī)內(nèi)存,動態(tài)分配 |
| 訪問速度 | 極快(直接CPU訪問) | 較慢(通過指針間接訪問) |
| 大小限制 | ?。ㄍǔ?-8MB) | 大(可達(dá)GB級) |
| 生命周期 | 函數(shù)/塊作用域結(jié)束自動釋放 | 由垃圾回收器(GC)管理 |

如上圖所示,兩種數(shù)據(jù)類型的內(nèi)存訪問流程如下所示:
- 基本數(shù)據(jù)類型的變量直接從棧內(nèi)存中獲取數(shù)據(jù)
- 引用數(shù)據(jù)類型的變量訪問過程:
- 讀取棧內(nèi)存中的指針(地址)
- 通過指針找到堆內(nèi)存中存儲的實(shí)際數(shù)據(jù)
二、不同類型的拷貝行為
1. 基本數(shù)據(jù)類型的拷貝
基本數(shù)據(jù)類型的拷貝非常簡單,由于它們直接存儲在棧內(nèi)存中,拷貝時會直接復(fù)制值本身,不存在引用關(guān)系。
let a = 10; let b = a; // 直接復(fù)制值 b = 20; console.log(a); // 10(不受 b 修改影響) console.log(b); // 20
2. 引用數(shù)據(jù)類型的拷貝
方式一:引用賦值(非拷貝)
引用數(shù)據(jù)類型在賦值時,默認(rèn)是引用賦值(即復(fù)制指針地址),而非復(fù)制實(shí)際內(nèi)容。這意味著兩個變量會指向堆內(nèi)存中的同一個對象。修改其中一個另一個會受影響。
let a = [1, 2, 5]; let b = a; // 引用賦值(復(fù)制指針) a[1] = 4; // 修改 a 指向的數(shù)組 console.log(a); // [1, 4, 5] console.log(b); // [1, 4, 5](b 也受影響) console.log(a === b); // true(指向同一個對象)

方式二: 淺拷貝
淺拷貝是針對引用類型的拷貝方式,它會創(chuàng)建一個新對象,但只復(fù)制對象的第一層屬性。其規(guī)則是:
- 對基本類型屬性:直接復(fù)制值
- 對引用類型屬性:僅復(fù)制指針(不復(fù)制指向的對象本身)
示例1:對數(shù)組 a = [1, 2, [3, 4], 5] 進(jìn)行淺拷貝得到 b 后:
b[0]、b[1]、b[3]是基本類型,修改它們不會影響ab[2]是引用類型(數(shù)組),修改b[2]會同時影響a[2],因?yàn)樗鼈冎赶蛲粋€子數(shù)組
const a = [1, 2, [3, 4], 5]; const b = [...a]; // 淺拷貝 a[0] = 100; console.log(b[0]); // 1(不受影響,基本類型獨(dú)立) a[2][1] = 400; // 修改子數(shù)組元素 console.log(b[2][1]); // 400(受影響,共享子數(shù)組引用)
示例2:
// 原始對象
const a = {
name: "alice", // 基本類型(棧內(nèi)存存儲值)
profile: { // 引用類型(堆內(nèi)存存儲對象,棧內(nèi)存存儲指針)
age: 25,
city: "beijing"
}
};
// 使用擴(kuò)展運(yùn)算符進(jìn)行淺拷貝
const b = { ...a };
// 修改淺拷貝對象
b.name = "bob";
b.profile.age = 30;
b.profile.city = "shanghai";
// 查看結(jié)果
console.log("原始對象 a.name:", a.name);
// 輸出:"alice"(基本類型值獨(dú)立,不受影響)
console.log("原始對象 a.profile.age:", a.profile.age);
// 輸出:30(引用類型共享堆內(nèi)存,被修改)
console.log("原始對象 a.profile.city:", a.profile.city);
// 輸出:"shanghai"(引用類型共享堆內(nèi)存,被修改)
console.log("a.profile === b.profile:", a.profile === b.profile);
// 輸出:true(兩者指向堆中同一個對象)
常見的淺拷貝方法:
- 擴(kuò)展運(yùn)算符(推薦)
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };
- Object.assign()
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);
- 數(shù)組的 slice()、concat() 方法
const arr = [1, 2, { a: 3 }];
const shallowCopy1 = arr.slice();
const shallowCopy2 = [].concat(arr);
- Array.from()
const arr = [1, 2, { a: 3 }];
const shallowCopy = Array.from(arr);
方式三、深拷貝
深拷貝會創(chuàng)建一個全新的對象,完全復(fù)制原始對象的所有層級屬性,包括嵌套的引用類型,使得新舊對象完全獨(dú)立。
實(shí)現(xiàn)方式 1:JSON 方法(最常用但有局限)
// 原始對象
const a = {
name: "alice", // 基本類型(棧內(nèi)存存儲值)
profile: { // 引用類型(堆內(nèi)存存儲對象)
age: 25,
city: "beijing"
}
};
// 實(shí)現(xiàn)深拷貝
const deepCopy = JSON.parse(JSON.stringify(a));
// 修改深拷貝對象的屬性
deepCopy.name = "bob"; // 修改基本類型
deepCopy.profile.age = 30; // 修改嵌套引用類型
deepCopy.profile.city = "shanghai";
// 查看結(jié)果對比
console.log("原始對象 a.name:", a.name);
// 輸出:"alice"(基本類型不受影響)
console.log("原始對象 a.profile.age:", a.profile.age);
// 輸出:25(嵌套引用類型也不受影響)
console.log("原始對象 a.profile.city:", a.profile.city);
// 輸出:"beijing"(嵌套引用類型完全獨(dú)立)
console.log("a.profile === deepCopy.profile:", a.profile === deepCopy.profile);
// 輸出:false(兩者指向堆中不同對象)
注意限制:
- 無法復(fù)制函數(shù)、undefined、Symbol
- 日期對象會被轉(zhuǎn)換為字符串
- 無法處理循環(huán)引用
- 會丟失原型鏈信息
實(shí)現(xiàn)方式 2:遞歸實(shí)現(xiàn)(自定義深拷貝函數(shù))
由于遞歸實(shí)現(xiàn)較為復(fù)雜,這里不展開詳細(xì)代碼,但基本原理是遍歷對象的所有屬性,對引用類型屬性遞歸調(diào)用拷貝函數(shù),直到所有層級都被復(fù)制。
三、深淺拷貝對比總結(jié)
| 類型 | 引用類型 | 內(nèi)存地址 | 第一層修改 | 第二層修改 |
|---|---|---|---|---|
| 引用賦值 | 引用復(fù)制 | 相同 | 相互影響 | 相互影響 |
| 淺拷貝 | 僅第一層值復(fù)制,嵌套層引用復(fù)制 | 不同 | 獨(dú)立 | 相互影響 |
| 深拷貝 | 完全復(fù)制 | 不同 | 獨(dú)立 | 獨(dú)立 |
深淺拷貝的核心區(qū)別在于對嵌套引用類型的處理方式,這直接決定了拷貝后對象的獨(dú)立性:
- 引用賦值:本質(zhì)上不是拷貝,只是復(fù)制了對象的引用指針。兩個變量共享同一塊堆內(nèi)存,任何層級的修改都會相互影響,內(nèi)存地址相同。
- 淺拷貝:創(chuàng)建新的內(nèi)存地址存儲對象,但僅對第一層屬性進(jìn)行值復(fù)制。對于基本類型屬性,修改后彼此獨(dú)立;但對于嵌套的引用類型屬性,仍共享原始引用,修改會相互影響。
- 深拷貝:完全創(chuàng)建新的對象,遞歸復(fù)制所有層級的屬性(包括嵌套引用類型)。新舊對象擁有完全獨(dú)立的內(nèi)存空間,任何層級的修改都不會相互影響,內(nèi)存地址不同。
到此這篇關(guān)于JavaScript中引用賦值、淺拷貝和深拷貝的文章就介紹到這了,更多相關(guān)JS引用賦值、淺拷貝和深拷貝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js實(shí)現(xiàn)加載頁面就自動觸發(fā)超鏈接的示例
下面小編就為大家?guī)硪黄猨s實(shí)現(xiàn)加載頁面就自動觸發(fā)超鏈接的示例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08
js實(shí)現(xiàn)帶緩沖效果的仿QQ面板折疊菜單代碼
這篇文章主要介紹了js實(shí)現(xiàn)帶緩沖效果的仿QQ面板折疊菜單代碼,通過JavaScript定時函數(shù)遞歸調(diào)用實(shí)現(xiàn)折疊菜單的緩沖效果,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09
原生js實(shí)現(xiàn)隨機(jī)點(diǎn)名功能
這篇文章主要為大家詳細(xì)介紹了原生js實(shí)現(xiàn)隨機(jī)點(diǎn)名功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-11-11
JS實(shí)現(xiàn)仿PS的調(diào)色板效果完整實(shí)例
這篇文章主要介紹了JS實(shí)現(xiàn)仿PS的調(diào)色板效果,結(jié)合完整實(shí)例形式分析了javascript通過運(yùn)算與動態(tài)操作頁面元素實(shí)現(xiàn)調(diào)色板功能的相關(guān)操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下2016-12-12
JavaScript中內(nèi)存泄漏的介紹與教程(推薦)
內(nèi)存泄露是指一塊被分配的內(nèi)存既不能使用,又不能回收,直到瀏覽器進(jìn)程結(jié)束。下面這篇文章主要給的大家介紹了關(guān)于JavaScript中內(nèi)存泄漏的相關(guān)資料,文中介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-06-06
JavaScript實(shí)現(xiàn)鼠標(biāo)經(jīng)過表格某行時此行變色
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)鼠標(biāo)經(jīng)過表格某行時此行變色,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-11-11

