面試官問(wèn):什么變量保存在堆/棧中?
什么變量保存在堆/棧中?
看到這個(gè)問(wèn)題,第一反應(yīng)表示很簡(jiǎn)單,基本類型保存在棧中,引用類型保存到堆中??????,但僅僅就如此簡(jiǎn)單嗎?我們接下來(lái)詳細(xì)看一看
JS 數(shù)據(jù)類型
我們知道 JS 就是動(dòng)態(tài)語(yǔ)言,因?yàn)樵诼暶髯兞恐安⒉恍枰_認(rèn)其數(shù)據(jù)類型,所以 JS 的變量是沒(méi)有數(shù)據(jù)類型的,值才有數(shù)據(jù)類型,變量可以隨時(shí)持有任何類型的數(shù)據(jù) 。
JS 值有 8 種數(shù)據(jù)類型:
Boolean:有 true和falseUndefined:沒(méi)有被賦值的變量或變量被提升時(shí)的,都會(huì)有個(gè)默認(rèn)值 undefinedNull:只有一個(gè)值 nullNumber:數(shù)字類型 BigInt(ES10):表示大于 253 - 1的整數(shù)String:字符類型 Symbol(ES6) Object:對(duì)象類型
其中前 7 種數(shù)據(jù)類型稱為基本類型,把最后一個(gè)對(duì)象類型稱為引用類型
JS中的變量存儲(chǔ)機(jī)制
JS 內(nèi)存空間分為棧(stack)空間、堆(heap)空間、代碼空間。其中代碼空間用于存放可執(zhí)行代碼。
棧空間
棧是內(nèi)存中一塊用于存儲(chǔ)局部變量和函數(shù)參數(shù)的線性結(jié)構(gòu),遵循著先進(jìn)后出 (LIFO / Last In First Out) 的原則。棧由內(nèi)存中占據(jù)一片連續(xù)的存儲(chǔ)空間,出棧與入棧僅僅是指針在內(nèi)存中的上下移動(dòng)而已。
JS 的棧空間就是我們所說(shuō)的調(diào)用棧,是用來(lái)存儲(chǔ)執(zhí)行上下文的,包含變量空間與詞法環(huán)境,var、function保存在變量環(huán)境,let、const 聲明的變量保存在詞法環(huán)境中。
var a = 1
function add(a) {
var b = 2
let c = 3
return a + b + c
}
// 函數(shù)調(diào)用
add(a)
這段代碼很簡(jiǎn)單,就是創(chuàng)建了一個(gè) add 函數(shù),然后調(diào)用了它。
下面我們就一步步的介紹整個(gè)函數(shù)調(diào)用執(zhí)行的過(guò)程。
在執(zhí)行這段代碼之前,JavaScript 引擎會(huì)先創(chuàng)建一個(gè)全局執(zhí)行上下文,包含所有已聲明的函數(shù)與變量:

從圖中可以看出,代碼中的全局變量 a 及函數(shù) add 保存在變量環(huán)境中。
執(zhí)行上下文準(zhǔn)備好后,開始執(zhí)行全局代碼,首先執(zhí)行 a = 1 的賦值操作,

賦值完成后 a 的值由 undefined 變?yōu)?1,然后執(zhí)行 add 函數(shù),JavaScript 判斷出這是一個(gè)函數(shù)調(diào)用,然后執(zhí)行以下操作:
首先,從全局執(zhí)行上下文中,取出 add 函數(shù)代碼 其次,對(duì) add 函數(shù)的這段代碼進(jìn)行編譯,并創(chuàng)建該函數(shù)的執(zhí)行上下文和可執(zhí)行代碼,并將執(zhí)行上下文壓入棧中

然后,執(zhí)行代碼,返回結(jié)果,并將 add 的執(zhí)行上下文也會(huì)從棧頂部彈出,此時(shí)調(diào)用棧中就只剩下全局上下文了。

至此,整個(gè)函數(shù)調(diào)用執(zhí)行結(jié)束了。
上面需要注意的是:函數(shù)(add)在存放在棧區(qū)的數(shù)據(jù),在函數(shù)調(diào)用結(jié)束后,就已經(jīng)自動(dòng)的出棧,換句話說(shuō):棧中的變量在函數(shù)調(diào)用結(jié)束后,就會(huì)自動(dòng)回收。
所以,通常棧空間都不會(huì)設(shè)置太大,而基本類型在內(nèi)存中占有固定大小的空間,所以它們的值保存在棧空間,我們通過(guò) 按值訪問(wèn) 。它們也不需要手動(dòng)管理,函數(shù)調(diào)時(shí)創(chuàng)建,調(diào)用結(jié)束則消失。
堆
堆數(shù)據(jù)結(jié)構(gòu)是一種樹狀結(jié)構(gòu)。它的存取數(shù)據(jù)的方式與書架和書非常相似。我們只需要知道書的名字就可以直接取出書了,并不需要把上面的書取出來(lái)。
在棧中存儲(chǔ)不了的數(shù)據(jù)比如對(duì)象就會(huì)被存儲(chǔ)在堆中,在棧中只是保留了對(duì)象在堆中的地址,也就是對(duì)象的引用 ,對(duì)于這種,我們把它叫做 按引用訪問(wèn) 。
舉個(gè)例子幫助理解一下:
var a = 1
function foo() {
var b = 2
var c = { name: 'an' } // 引用類型
}
// 函數(shù)調(diào)用
foo()

所以,堆空間通常很大,能存放很多大的數(shù)據(jù),不過(guò)缺點(diǎn)是分配內(nèi)存和回收內(nèi)存都會(huì)占用一定的時(shí)間
JS中的變量存儲(chǔ)機(jī)制與閉包
對(duì)以上總結(jié)一下,JS 內(nèi)存空間分為棧(stack)空間、堆(heap)空間、代碼空間。其中代碼空間用于存放可執(zhí)行代碼
基本類型:保存在棧內(nèi)存中,因?yàn)檫@些類型在存中分別占有固定大小的空間,通過(guò)按值來(lái)訪問(wèn)。 引用類型:保存在堆內(nèi)存中,因?yàn)檫@種值的大小不固定,因此不能把它們保存到棧內(nèi)存中,但內(nèi)存地址大小的固定的,因此保存在堆內(nèi)存中,在棧內(nèi)存中存放的只是該對(duì)象的訪問(wèn)地址。當(dāng)查詢引用類型的變量時(shí), 先從棧中讀取內(nèi)存地址, 然后再通過(guò)地址找到堆中的值。對(duì)于這種,我們把它叫做按引用訪問(wèn)。
閉包
那么閉包喃?既然基本類型變量存儲(chǔ)在棧中,棧中數(shù)據(jù)在函數(shù)執(zhí)行完成后就會(huì)被自動(dòng)銷毀,那執(zhí)行函數(shù)之后為什么閉包還能引用到函數(shù)內(nèi)的變量?
function foo() {
let num = 1 // 創(chuàng)建局部變量 num 和局部函數(shù) bar
function bar() { // bar()是函數(shù)內(nèi)部方法,是一個(gè)閉包
num++
console.log(num) // 使用了外部函數(shù)聲明的變量,內(nèi)部函數(shù)可以訪問(wèn)外部函數(shù)的變量
}
return bar // bar 被外部函數(shù)作為返回值返回了,返回的是一個(gè)閉包
}
// 測(cè)試
let test = foo()
test() // 2
test() // 3
在執(zhí)行完函數(shù) foo 后,foo 中的變量 num 應(yīng)該被彈出銷毀,為什么還能繼續(xù)使用喃?
這說(shuō)明閉包中的變量沒(méi)有保存在棧中,而是保存到了堆中:
console.dir(test)

所以 JS 引擎判斷當(dāng)前是一個(gè)閉包時(shí),就會(huì)在堆空間創(chuàng)建換一個(gè)“closure(foo)”的對(duì)象(這是一個(gè)內(nèi)部對(duì)象,JS 是無(wú)法訪問(wèn)的),用來(lái)保存 num 變量
注意,即使不返回函數(shù)(閉包沒(méi)有被返回):
function foo() {
let num = 1 // 創(chuàng)建局部變量 num 和局部函數(shù) bar
function bar() { // bar()是函數(shù)內(nèi)部方法,是一個(gè)閉包
num++
console.log(num) // 使用了外部函數(shù)聲明的變量,內(nèi)部函數(shù)可以訪問(wèn)外部函數(shù)的變量
}
bar() // 2
bar() // 3
console.dir(bar)
}
foo()

總結(jié)
JS 就是動(dòng)態(tài)語(yǔ)言,因?yàn)樵诼暶髯兞恐安⒉恍枰_認(rèn)其數(shù)據(jù)類型,所以 JS 的變量是沒(méi)有數(shù)據(jù)類型的,值才有數(shù)據(jù)類型,變量可以隨時(shí)持有任何類型的數(shù)據(jù).
JS 值有 8 種數(shù)據(jù)類型,它們可以分為兩大類——基本類型和引用類型。其中,基本類型的數(shù)據(jù)是存放在棧中,引用類型的數(shù)據(jù)是存放在堆中的。堆中的數(shù)據(jù)是通過(guò)引用和變量關(guān)聯(lián)起來(lái)的。
閉包除外,JS 閉包中的變量值并不保存中棧內(nèi)存中,而是保存在堆內(nèi)存中。
來(lái)自:https://github.com/Advanced-Frontend/Daily-Interview-Question
