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

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

方式二: 淺拷貝
淺拷貝是針對(duì)引用類(lèi)型的拷貝方式,它會(huì)創(chuàng)建一個(gè)新對(duì)象,但只復(fù)制對(duì)象的第一層屬性。其規(guī)則是:
- 對(duì)基本類(lèi)型屬性:直接復(fù)制值
- 對(duì)引用類(lèi)型屬性:僅復(fù)制指針(不復(fù)制指向的對(duì)象本身)
示例1:對(duì)數(shù)組 a = [1, 2, [3, 4], 5] 進(jìn)行淺拷貝得到 b 后:
b[0]、b[1]、b[3]是基本類(lèi)型,修改它們不會(huì)影響ab[2]是引用類(lèi)型(數(shù)組),修改b[2]會(huì)同時(shí)影響a[2],因?yàn)樗鼈冎赶蛲粋€(gè)子數(shù)組
const a = [1, 2, [3, 4], 5]; const b = [...a]; // 淺拷貝 a[0] = 100; console.log(b[0]); // 1(不受影響,基本類(lèi)型獨(dú)立) a[2][1] = 400; // 修改子數(shù)組元素 console.log(b[2][1]); // 400(受影響,共享子數(shù)組引用)
示例2:
// 原始對(duì)象
const a = {
name: "alice", // 基本類(lèi)型(棧內(nèi)存存儲(chǔ)值)
profile: { // 引用類(lèi)型(堆內(nèi)存存儲(chǔ)對(duì)象,棧內(nèi)存存儲(chǔ)指針)
age: 25,
city: "beijing"
}
};
// 使用擴(kuò)展運(yùn)算符進(jìn)行淺拷貝
const b = { ...a };
// 修改淺拷貝對(duì)象
b.name = "bob";
b.profile.age = 30;
b.profile.city = "shanghai";
// 查看結(jié)果
console.log("原始對(duì)象 a.name:", a.name);
// 輸出:"alice"(基本類(lèi)型值獨(dú)立,不受影響)
console.log("原始對(duì)象 a.profile.age:", a.profile.age);
// 輸出:30(引用類(lèi)型共享堆內(nèi)存,被修改)
console.log("原始對(duì)象 a.profile.city:", a.profile.city);
// 輸出:"shanghai"(引用類(lèi)型共享堆內(nèi)存,被修改)
console.log("a.profile === b.profile:", a.profile === b.profile);
// 輸出:true(兩者指向堆中同一個(gè)對(duì)象)
常見(jiàn)的淺拷貝方法:
- 擴(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);
方式三、深拷貝
深拷貝會(huì)創(chuàng)建一個(gè)全新的對(duì)象,完全復(fù)制原始對(duì)象的所有層級(jí)屬性,包括嵌套的引用類(lèi)型,使得新舊對(duì)象完全獨(dú)立。
實(shí)現(xiàn)方式 1:JSON 方法(最常用但有局限)
// 原始對(duì)象
const a = {
name: "alice", // 基本類(lèi)型(棧內(nèi)存存儲(chǔ)值)
profile: { // 引用類(lèi)型(堆內(nèi)存存儲(chǔ)對(duì)象)
age: 25,
city: "beijing"
}
};
// 實(shí)現(xiàn)深拷貝
const deepCopy = JSON.parse(JSON.stringify(a));
// 修改深拷貝對(duì)象的屬性
deepCopy.name = "bob"; // 修改基本類(lèi)型
deepCopy.profile.age = 30; // 修改嵌套引用類(lèi)型
deepCopy.profile.city = "shanghai";
// 查看結(jié)果對(duì)比
console.log("原始對(duì)象 a.name:", a.name);
// 輸出:"alice"(基本類(lèi)型不受影響)
console.log("原始對(duì)象 a.profile.age:", a.profile.age);
// 輸出:25(嵌套引用類(lèi)型也不受影響)
console.log("原始對(duì)象 a.profile.city:", a.profile.city);
// 輸出:"beijing"(嵌套引用類(lèi)型完全獨(dú)立)
console.log("a.profile === deepCopy.profile:", a.profile === deepCopy.profile);
// 輸出:false(兩者指向堆中不同對(duì)象)
注意限制:
- 無(wú)法復(fù)制函數(shù)、undefined、Symbol
- 日期對(duì)象會(huì)被轉(zhuǎn)換為字符串
- 無(wú)法處理循環(huán)引用
- 會(huì)丟失原型鏈信息
實(shí)現(xiàn)方式 2:遞歸實(shí)現(xiàn)(自定義深拷貝函數(shù))
由于遞歸實(shí)現(xiàn)較為復(fù)雜,這里不展開(kāi)詳細(xì)代碼,但基本原理是遍歷對(duì)象的所有屬性,對(duì)引用類(lèi)型屬性遞歸調(diào)用拷貝函數(shù),直到所有層級(jí)都被復(fù)制。
三、深淺拷貝對(duì)比總結(jié)
| 類(lèi)型 | 引用類(lèi)型 | 內(nèi)存地址 | 第一層修改 | 第二層修改 |
|---|---|---|---|---|
| 引用賦值 | 引用復(fù)制 | 相同 | 相互影響 | 相互影響 |
| 淺拷貝 | 僅第一層值復(fù)制,嵌套層引用復(fù)制 | 不同 | 獨(dú)立 | 相互影響 |
| 深拷貝 | 完全復(fù)制 | 不同 | 獨(dú)立 | 獨(dú)立 |
深淺拷貝的核心區(qū)別在于對(duì)嵌套引用類(lèi)型的處理方式,這直接決定了拷貝后對(duì)象的獨(dú)立性:
- 引用賦值:本質(zhì)上不是拷貝,只是復(fù)制了對(duì)象的引用指針。兩個(gè)變量共享同一塊堆內(nèi)存,任何層級(jí)的修改都會(huì)相互影響,內(nèi)存地址相同。
- 淺拷貝:創(chuàng)建新的內(nèi)存地址存儲(chǔ)對(duì)象,但僅對(duì)第一層屬性進(jìn)行值復(fù)制。對(duì)于基本類(lèi)型屬性,修改后彼此獨(dú)立;但對(duì)于嵌套的引用類(lèi)型屬性,仍共享原始引用,修改會(huì)相互影響。
- 深拷貝:完全創(chuàng)建新的對(duì)象,遞歸復(fù)制所有層級(jí)的屬性(包括嵌套引用類(lèi)型)。新舊對(duì)象擁有完全獨(dú)立的內(nèi)存空間,任何層級(jí)的修改都不會(huì)相互影響,內(nèi)存地址不同。
到此這篇關(guān)于JavaScript中引用賦值、淺拷貝和深拷貝的文章就介紹到這了,更多相關(guān)JS引用賦值、淺拷貝和深拷貝內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript 數(shù)組常見(jiàn)操作技巧 (二)
這篇文章主要介紹了JavaScript 數(shù)組常見(jiàn)操作技巧,上一篇文章已經(jīng)給大家分享了(一),下面緊接上一篇文章分享下面技巧,需要的小伙伴可以參考一下2022-02-02
JavaScript中instanceof與typeof運(yùn)算符的用法及區(qū)別詳細(xì)解析
這篇文章主要是對(duì)JavaScript中instanceof與typeof運(yùn)算符的用法及區(qū)別進(jìn)行了詳細(xì)的分析介紹。需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-11-11
JavaScript中的連續(xù)賦值問(wèn)題實(shí)例分析
這篇文章主要介紹了JavaScript中的連續(xù)賦值問(wèn)題,結(jié)合實(shí)例形式分析了javascript賦值語(yǔ)句以及連續(xù)賦值操作相關(guān)用法與操作注意事項(xiàng),需要的朋友可以參考下2019-07-07
javascript實(shí)現(xiàn)fetch請(qǐng)求返回的統(tǒng)一攔截
這篇文章主要介紹了javascript實(shí)現(xiàn)fetch請(qǐng)求返回的統(tǒng)一攔截,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
利用select實(shí)現(xiàn)年月日三級(jí)聯(lián)動(dòng)的日期選擇效果【推薦】
關(guān)于select控件,可能年月日三級(jí)聯(lián)動(dòng)的日期選擇效果是最常見(jiàn)的應(yīng)用了。本文將對(duì)日期選擇效果進(jìn)行詳細(xì)介紹。需要的朋友一起來(lái)看下吧2016-12-12
基于JS實(shí)現(xiàn)二維碼圖片固定在右下角某處并跟隨滾動(dòng)條滾動(dòng)
這篇文章主要介紹了基于JS實(shí)現(xiàn)二維碼圖片固定在右下角某處并跟隨滾動(dòng)條滾動(dòng),代碼簡(jiǎn)單易懂非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02
Javascript中3個(gè)需要注意的運(yùn)算符
這篇文章主要介紹了Javascript中3個(gè)需要注意的運(yùn)算符,這3個(gè)運(yùn)算符的使用有很多需要注意的地方和有意思的地方,需要的朋友可以參考下2015-04-04

