JavaScript 內(nèi)存中的堆和棧
棧(stack):堆是 JavaScript 用來存儲(chǔ)靜態(tài)數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)扶关。靜態(tài)數(shù)據(jù)是引擎在編譯時(shí)知道其大小的數(shù)據(jù)佛纫。截止 ES2021, 在 JavaScript 中奏候,這包括 7 種原始值(Primitive values)(string, number, boolean, bull, undefined, bigInt, symbol)和指向?qū)ο蠛秃瘮?shù)的引用哥力。
堆(heap):堆是一個(gè)不同的存儲(chǔ)數(shù)據(jù)的空間锣枝,JavaScript 在這里存儲(chǔ)對(duì)象和函數(shù)迎卤。
定義
將一變量的值賦值給另一個(gè)變量拴鸵,相當(dāng)于在棧內(nèi)存中創(chuàng)建了一個(gè)新的內(nèi)存空間,然后從棧中復(fù)制值蜗搔,存儲(chǔ)到這個(gè)新空間中劲藐。
1. 對(duì)于基本類型,棧中存儲(chǔ)的就是它自身的值樟凄,所以新內(nèi)存空間存儲(chǔ)的也是一個(gè)值聘芜。直接改變新變量的值,不會(huì)影響到舊變量的值缝龄,因?yàn)樗麄冎荡鎯?chǔ)的內(nèi)存空間不同汰现。
因此將基本變量 a 的值復(fù)制給另一個(gè)變量 b 時(shí),改變 b 的值并不會(huì)影響原來的變量 a 本身的值叔壤。
2. 對(duì)于引用變量瞎饲,棧中主要存儲(chǔ)它所引用的對(duì)象的地址。
因此炼绘,將引用變量a的值復(fù)制給另一個(gè)變量b時(shí)嗅战,實(shí)際上a和b都是指向堆中同一個(gè)內(nèi)存地址。因此改變b的值俺亮,相當(dāng)于改變b指向的堆中的值仗哨,因此a也會(huì)隨之改變。
所以我們一般談深拷貝和淺拷貝都是針對(duì)于引用類型而言的铅辞。因?yàn)獒槍?duì)值類型厌漂,它的拷貝結(jié)果都是深拷貝。
淺拷貝(shallow copy):當(dāng)將舊引用變量的值賦給新引用變量時(shí)斟珊,將舊引用變量中存儲(chǔ)的地址復(fù)制到新引用變量中苇倡。這意味著舊的和新的引用變量都指向內(nèi)存中的相同對(duì)象。因此囤踩,如果對(duì)象的狀態(tài)通過任何一個(gè)引用變量發(fā)生變化旨椒,它就會(huì)同時(shí)反映這兩個(gè)變量。
深拷貝(deep copy):深拷貝會(huì)復(fù)制舊對(duì)象的所有成員堵漱,為新對(duì)象分配單獨(dú)的內(nèi)存位置综慎,然后將復(fù)制的成員分配給新對(duì)象。這樣勤庐,兩個(gè)對(duì)象都是相互獨(dú)立的示惊,如果對(duì)其中一個(gè)進(jìn)行任何修改好港,另一個(gè)對(duì)象也不會(huì)受到影響。
簡而言之米罚,如果把對(duì)象 a 復(fù)制給另一個(gè)對(duì)象 b钧汹,如果修改 b,a 也隨之改變了录择,就是淺拷貝拔莱。相反,如果 a 維持原始值隘竭,則是深拷貝塘秦。
淺拷貝實(shí)現(xiàn)
先聲明如下一個(gè)引用類型對(duì)象person
:
let person = { name: "Emon", job: "developer" };
1. 直接賦值
let shallowCopyPerson = person;
2. Object.assign(target)
`Object.assign(target, source1, source2...)`原本是將所有可枚舉屬性的值從一個(gè)或多個(gè)源對(duì)象分配到目標(biāo)對(duì)象。返回目標(biāo)對(duì)象动看。
通常用法:
const targetObj = { a: 1, b: 2 };
const sourceObj = { c: 3, d: 4 };
Object.assign(targetObj, sourceObj); // {a: 1, b: 2, c: 3, d: 4}
console.log(targetObj); //{a: 1, b: 2, c: 3, d: 4}
這里我們可用用來復(fù)制對(duì)象:
let shallowCopyPerson = Object.assign(person);
1. 遍歷賦值嗤形,這種方式雖然把對(duì)象進(jìn)行了遍歷,但是本質(zhì)還是復(fù)制的是對(duì)象的引用弧圆。
這幾個(gè)的復(fù)制的結(jié)果都是復(fù)制的都是對(duì)指針的復(fù)制赋兵,因此改變shallowCopyPerson
的屬性后,原來對(duì)象person
的屬性也會(huì)隨之改變搔预,結(jié)果如下:
shallowCopyPerson.name = "lucy"; // 改變復(fù)制后的值
console.log(shallowCopyPerson); // {name: 'lucy', job: 'developer'} 復(fù)制對(duì)象發(fā)生改變霹期。
console.log(person); // {name: 'lucy', job: 'developer'} 注意目標(biāo)對(duì)象自身也會(huì)改變。
深拷貝實(shí)現(xiàn)
其實(shí)有很多方法可以實(shí)現(xiàn)深拷貝拯田,這里我們簡單介紹幾種比較方便高效的历造。
- Object.assign({}, source);
這個(gè)方法是和上面的淺拷貝實(shí)現(xiàn)稍有區(qū)別,他把 target 設(shè)置為空對(duì)象船庇,在將 source 對(duì)象傳進(jìn)去吭产,實(shí)現(xiàn)了復(fù)制
let deepCopyPerson = Object.assign({}, person);
- ...拓展運(yùn)算符(spread operator)
let deepCopyPerson = { ...person };
- JSON.parse/stringify
這個(gè)方法的限制比較多,如果你需要拷貝的對(duì)象中沒有functions
,undefined
,Infinit
或者像RegExps
,Maps
,Sets
,Blobs
,FileLists
,ImageDatas
等比較復(fù)雜的類型鸭轮。那么可以用這個(gè)方法臣淤。
const a = {
string: "string",
number: 123,
bool: false,
nul: null,
date: new Date(), // stringified
undef: undefined, // lost
inf: Infinity, // forced to 'null'
re: /.*/, // lost, to {}
fun: () => "hello world", // lost
};
- 遍歷賦值
我們可以通過遍歷原有對(duì)象,把里面的值一個(gè)一個(gè)重新賦值給新對(duì)象窃爷。
let shallowCopyPerson = {};
for (const key in person) {
shallowCopyPerson[key] = person[key];
}
-
structuredClone()
方法
這是 JS 官方定義的方法邑蒋。不過遺憾的是,目前主流瀏覽器都不支持這個(gè)方法按厘∫降酰可以參考structuredClone獲取最新的瀏覽器支持結(jié)果。
const shallowCopyPerson = structuredClone(person);