在開發(fā)過程中我們對(duì)基本類型和引用類型的一些特性有所了解:
基本類型是按值訪問的蹬屹,所以我們可以盡情地賦值修改不用擔(dān)心有副作用意述;
而引用類型是按引用訪問的宣吱,所以這會(huì)導(dǎo)致當(dāng)我們操作一個(gè)復(fù)制自另一個(gè)引用類型的值時(shí)陡鹃,使得另一個(gè)值也發(fā)生變化。
說起來可能有點(diǎn)拗口疚脐,又是復(fù)制又是賦值的,還有什么按值訪問按引用訪問邢疙,看一下demo:
// 聲明兩個(gè)基本類型的變量棍弄,simpleVariable2復(fù)制自simpleVariable1
let simpleVariable1 = 'simpleVariable1';
let simpleVariable2 = simpleVariable1;
// 打印出兩個(gè)變量的值,沒有什么問題
console.log(simpleVariable1); // simpleVariable1
console.log(simpleVariable2); // simpleVariable1
// 修改 simpleVariable2 的值疟游,一切如預(yù)期
simpleVariable2 = 'simpleVariable2';
console.log(simpleVariable1); // simpleVariable1
console.log(simpleVariable2); // simpleVariable2
// 聲明兩個(gè)復(fù)雜類型的變量呼畸,complexVariable2復(fù)制自complexVariable1
let complexVariable1 = {
name: 'complexVariable1',
};
let complexVariable2 = complexVariable1;
// 打印出兩個(gè)變量的值,沒有什么問題
console.log(complexVariable1); // name:complexVariable1
console.log(complexVariable2); // name:complexVariable1
// 修改complexVariable2中的值颁虐,一切就不如預(yù)期了:complexVariable1中的值也發(fā)生了變化
complexVariable2.name = 'complexVariable2';
console.log(complexVariable1); // name:complexVariable2
console.log(complexVariable2); // name:complexVariable2
本文基于以上代碼反映的問題對(duì)基本類型和引用類型從內(nèi)存空間的角度做深入學(xué)習(xí)蛮原,從而對(duì)變量的聲明、引用過程有更深的理解另绩,間接達(dá)到提升代碼質(zhì)量的作用儒陨。
《JavaScript高級(jí)程序設(shè)計(jì)》中包含的這些內(nèi)容的章節(jié):4.1基本類型和引用類型的值
參考文章:
Does JavaScript use stack or heap for memory allocation or both?
前端基礎(chǔ)進(jìn)階(一):內(nèi)存空間詳細(xì)圖解
國(guó)內(nèi)外已經(jīng)有很多大神寫了相關(guān)的文章或回答,本文基于這些文章學(xué)習(xí)理解而得笋籽。簡(jiǎn)書上關(guān)于 內(nèi)存空間詳細(xì)圖解 的這篇文章已經(jīng)寫得非常詳盡蹦漠,因此本文僅對(duì)那篇文章中沒有提及或不夠完善的部分做補(bǔ)充。
棧內(nèi)存與堆內(nèi)存
棧內(nèi)存作為內(nèi)存中的一塊區(qū)域车海,遵循先進(jìn)后出First-In-Last-Out
的存儲(chǔ)模式津辩。一般棧內(nèi)存比較小,但是存取數(shù)據(jù)的效率遠(yuǎn)高于堆內(nèi)存,因此多用來存儲(chǔ)一些簡(jiǎn)單的數(shù)據(jù)喘沿。
堆內(nèi)存和棧內(nèi)存正好相反闸度,內(nèi)存大但是存取數(shù)據(jù)的效率低,數(shù)據(jù)存儲(chǔ)方式無序且隨意蚜印,因此適合存放一些復(fù)雜的數(shù)據(jù)結(jié)構(gòu)莺禁。
通過這一小段簡(jiǎn)單的說明,其實(shí)對(duì)ES中的數(shù)據(jù)類型可以有一些合理的猜想:
因?yàn)闂?nèi)存空間小存取速度快窄赋,多用來存儲(chǔ)一些簡(jiǎn)單的數(shù)據(jù)哟冬,那么ES中基本類型值很可能就存儲(chǔ)棧內(nèi)存中;
而因?yàn)槎褍?nèi)存的一些特性忆绰,使得它非常適合拿來存儲(chǔ)引用類型值浩峡。
在接下來章節(jié)中我們會(huì)去探究到底是不是這種分配方式。
ES中的內(nèi)存空間
在ECMA-262標(biāo)準(zhǔn)中沒有定義內(nèi)存布局错敢,因此本文討論的內(nèi)存空間是由JavaScript引擎實(shí)現(xiàn)的翰灾。
這里以V8引擎的內(nèi)存布局實(shí)現(xiàn)舉例。
從js垃圾回收機(jī)制來認(rèn)識(shí)它的內(nèi)存空間
參考文章Does JavaScript use stack or heap for memory allocation or both?中提到:
The easiest way is to think for a second. Javascript is a garbage-collected language. GC means that there is data scattered around which has to be cleaned up. Does that sound like the data is stored on the stack? Rather... no, because the stack has strictly ordered data. So, variables are allocated on the heap.
在MDN 內(nèi)存管理中也有提到:
像C語言這樣的高級(jí)語言一般都有底層的內(nèi)存管理接口稚茅,比如 malloc()和free()纸淮。另一方面,JavaScript創(chuàng)建變量(對(duì)象亚享,字符串等)時(shí)分配內(nèi)存咽块,并且在不再使用它們時(shí)“自動(dòng)”釋放。 后一個(gè)過程稱為垃圾回收欺税。這個(gè)“自動(dòng)”是混亂的根源侈沪,并讓JavaScript(和其他高級(jí)語言)開發(fā)者感覺他們可以不關(guān)心內(nèi)存管理。 這是錯(cuò)誤的晚凿。
JavaScript的垃圾回收機(jī)制意味著它會(huì)去不斷回收雜亂無序的不再使用的變量亭罪,而堆內(nèi)存嚴(yán)格的先進(jìn)后出
的存儲(chǔ)模式無法滿足垃圾回收機(jī)制的運(yùn)行要求--通過這點(diǎn)可以看出JavaScript中的變量都是存儲(chǔ)在堆內(nèi)存中的。
結(jié)論
既然已經(jīng)得出了結(jié)論那么為什么還有之后那么多章節(jié)晃虫,而且之后的章節(jié)中還提到了棧內(nèi)存皆撩,這和得出的結(jié)論是否矛盾呢?在翻了一些文章結(jié)合理解之后得出了如下較為系統(tǒng)的結(jié)論:
ES的標(biāo)準(zhǔn)中沒有明確定義內(nèi)存布局哲银,內(nèi)存布局由具體的JavaScript引擎實(shí)現(xiàn)扛吞。
通過內(nèi)存回收機(jī)制可以看出JavaScript中的變量都是存儲(chǔ)在堆內(nèi)存中的。
在之后章節(jié)中提到的
棧內(nèi)存
荆责,是由js引擎在堆內(nèi)存中模擬的一個(gè)類似于棧內(nèi)存的對(duì)象滥比。
內(nèi)存空間總覽
↑↑↑圖片來自內(nèi)存空間詳細(xì)圖解
基本類型的內(nèi)存空間
如上一節(jié)中猜測(cè)的一般:對(duì)于基本類型,其值直接存放在棧內(nèi)存中做院,我們可以直接通過變量名訪問到它們的值盲泛。因此書本中說
基本數(shù)據(jù)類型是按值訪問的濒持,因?yàn)榭梢圆僮鞅4嬖谧兞恐械膶?shí)際的值。
// 聲明一個(gè)基本類型的變量
const simpleVariable = 'simpleVariable';
// 由simpleVariable復(fù)制一個(gè)copySimpleVariable變量
let copySimpleVariable = simpleVariable;
// 因?yàn)榛绢愋偷淖兞渴前粗翟L問的寺滚,所以修改copySimpleVariable的值會(huì)改了copySimpleVariable的值不會(huì)對(duì)simpleVariable產(chǎn)生影響
copySimpleVariable = 123;
引用類型的內(nèi)存空間
而對(duì)于引用類型來說柑营,它們的值存放在堆內(nèi)存中,變量名保存的是一個(gè)內(nèi)存地址村视,當(dāng)我們?cè)L問這個(gè)變量的時(shí)候?qū)嶋H訪問的是一個(gè)引用地址官套,通過這個(gè)引用地址訪問到存在在堆內(nèi)存中的值。
//聲明一個(gè)引用類型
const m = {
name: 'complexVariable',
};
//聲明一個(gè)新的引用類型蚁孔,復(fù)制自complexVariable
const n = complexVariable;
如這段代碼所示奶赔,當(dāng)我們聲明一個(gè)復(fù)制自m的引用類型n時(shí),并不是像基本類型一般會(huì)直接修改這個(gè)變量的值杠氢,而是給這個(gè)變量一個(gè)指向堆內(nèi)存的引用地址的值站刑,如圖所示:
↑↑↑圖片來自內(nèi)存空間詳細(xì)圖解
當(dāng)操作引用類型值時(shí)內(nèi)存空間發(fā)生了什么變化
對(duì)引用類型的操作大體有如下幾種:
復(fù)制
增加、刪除鼻百、修改屬性
賦值
當(dāng)發(fā)生復(fù)制操作時(shí)绞旅,內(nèi)存空間如上節(jié)中提到的所示,當(dāng)我們聲明一個(gè)復(fù)制自 m的引用類型 n時(shí)愕宋,并不是像基本類型一般會(huì)直接修改這個(gè)變量的值玻靡,而是給這個(gè)變量一個(gè)指向堆內(nèi)存的引用地址的值结榄。此時(shí)發(fā)生的情況是書本中說的
引用類型的值是按引用訪問的中贝。
當(dāng)發(fā)生增加、刪除臼朗、修改屬性時(shí)邻寿,會(huì)通過變量的內(nèi)存地址的值訪問到在堆內(nèi)存中實(shí)際的對(duì)象,并對(duì)這個(gè)實(shí)際的對(duì)象進(jìn)行相應(yīng)的增加视哑、刪除绣否、修改屬性操作。此時(shí)的情況便是在書本下方加的注解
但在為對(duì)象添加屬性時(shí)挡毅,操作的是實(shí)際的對(duì)象蒜撮。
正因?yàn)槭遣僮髁藢?shí)際的對(duì)象,所以會(huì)導(dǎo)致所有指向這個(gè)堆內(nèi)存中對(duì)象的變量都發(fā)生變化跪呈,這就是文章開頭demo中說到的情況:
聲明兩個(gè)復(fù)雜類型的變量段磨,complexVariable2復(fù)制自complexVariable1。
修改complexVariable2中的值耗绿,一切就不如預(yù)期了:complexVariable1中的值也發(fā)生了變化
當(dāng)發(fā)生賦值操作時(shí)苹支,操作對(duì)象的值會(huì)賦值為堆內(nèi)存中將要賦值的對(duì)象的內(nèi)存地址。而此時(shí)就算操作的變量是復(fù)制自別的引用類型變量误阻,也會(huì)接觸兩個(gè)變量之間的關(guān)系债蜜,無論操作哪個(gè)對(duì)象都不會(huì)對(duì)另一個(gè)產(chǎn)生影響:
// 聲明一個(gè)引用類型變量
let complexVariable1 = {
name: 'complexVariable1',
code: '01',
};
// 聲明一個(gè)復(fù)制自complexVariable1的變量
let complexVariable2 = complexVariable1;
// 修改complexVariable2的name屬性晴埂,會(huì)操作在堆內(nèi)存中的實(shí)際對(duì)象導(dǎo)致complexVariable1也發(fā)生變化
complexVariable2.name = 'complexVariable2';
console.log(complexVariable1.name); // complexVariable2
// 而當(dāng)我們做賦值操作的時(shí)候,會(huì)將一個(gè)新的內(nèi)存地址賦值給complexVariable2
complexVariable2 = {
name: 'new complexVariable2',
code: '007',
}
// 現(xiàn)在complexVariable2和complexVariable1已經(jīng)沒有半毛錢關(guān)系了
complexVariable2.name = 'change back to complexVariable1';
console.log(complexVariable1.name); // complexVariable2
通過對(duì)內(nèi)存空間的學(xué)習(xí)寻定,可以清楚地知道變量發(fā)生創(chuàng)建賦值修改操作時(shí)js引擎做了哪些動(dòng)作儒洛,從而更好地把握并使用各個(gè)變量。