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

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

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

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

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

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

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

總結(jié)
JS 就是動態(tài)語言,因為在聲明變量之前并不需要確認(rèn)其數(shù)據(jù)類型,所以 JS 的變量是沒有數(shù)據(jù)類型的,值才有數(shù)據(jù)類型,變量可以隨時持有任何類型的數(shù)據(jù).
JS 值有 8 種數(shù)據(jù)類型,它們可以分為兩大類——基本類型和引用類型。其中,基本類型的數(shù)據(jù)是存放在棧中,引用類型的數(shù)據(jù)是存放在堆中的。堆中的數(shù)據(jù)是通過引用和變量關(guān)聯(lián)起來的。
閉包除外,JS 閉包中的變量值并不保存中棧內(nèi)存中,而是保存在堆內(nèi)存中。
來自:https://github.com/Advanced-Frontend/Daily-Interview-Question
