此次的 why what or how
主題:JavaScript
變量存儲。javascript
不管那門語言,變量是組成一切的基礎,一個數字是一個變量,一個對象也是一個變量,在 JavaScript
中甚至連一個函數都是一個變量。html
那麼如此重要的變量,在 JavaScript
中到底是如何進行存儲的?java
棧(
Stack
)又名堆棧,它是一種運算受限的線性表。限定僅在表尾進行插入和刪除操做的線性表。node
在百度上搜索 JavaScript
變量存儲,能看到不少文章,無外乎一個結論:程序員
對於原始類型,數據自己是存在棧內,對於對象類型,在棧中存的只是一個堆內地址的引用。面試
可是,我忽然想到一個問題:若是說原始類型存在在棧中,那麼 JavaScript
中的閉包是如何實現的?windows
固然想要深究這個問題,有必要先把棧(Stack
)和堆(Heap
)給說說清楚。瀏覽器
那好,先說說棧。數據結構
棧是內存中一塊用於存儲局部變量和函數參數的線性結構,遵循着先進後出的原則。數據只能順序的入棧,順序的出棧。固然,棧只是內存中一片連續區域一種形式化的描述,數據入棧和出棧的操做僅僅是棧指針在內存地址上的上下移動而已。以下圖所示(以 C 語言爲例):閉包
如圖所示,棧指針剛開始指向內存中 0x001
的位置,接着 sum
函數開始調用,因爲聲明瞭兩個變量,往棧中存放了兩個數值,棧指針也對應開始移動,當 sum
函數調用結束時,僅僅是把棧指針往下移動而已,並非真正的數據彈出,數據還在,只不過下次賦值時會被覆蓋。
挺簡單的不是麼,但須要註明一點的是:內存中棧區的數據,在函數調用結束後,就會自動的出棧,不須要程序進行操做,操做系統會自動執行,換句話說:棧中的變量在函數調用結束後,就會消失。
所以棧的特色:輕量,不須要手動管理,函數調時建立,調用結束則消失。
堆能夠簡單的認爲是一大塊內存空間,就像一個籃子,你往裏面放什麼都不要緊,可是籃子是私人物品,操做系統並不會管你的籃子裏都放了什麼,也不會主動去清理你的籃子,所以在 C
語言中,堆中內容是須要程序員手動清理的,否則就會出現內存溢出的狀況。
爲了必定程度的解決堆的問題,一些高級語言(如 JAVA
)提出了一個概念:GC
,Garbage Collection
,垃圾回收,用於協助程序管理內存,主動清理堆中已不被使用的數據。
既然堆是一個大大的籃子,那麼在棧中存儲不了的數據(好比一個對象),就會被存儲在堆中,棧中就僅僅保留一個對該數據的引用(也就是該塊數據的首地址)。
OK
棧和堆內容如上,如今咱們再來看看你們的結論:
對於原始類型,數據自己是存在棧內,對於對象類型,在棧中存的只是一個堆內地址的引用。
感受很符合邏輯啊,按照定義基礎類型存在棧中,對象存在堆中,沒毛病啊!
可是,請你們思考一個問題:
既然棧中數據在函數執行結束後就會被銷燬,那麼 JavaScript
中函數閉包該如何實現,先簡單來個閉包:
function count () {
let num = -1;
return function () {
num++;
return num;
}
}
let numCount = count();
numCount();
// 0
numCount();
// 1
複製代碼
按照結論,num
變量在調用 count
函數時建立,在 return
時從棧中彈出。
既然是這樣的邏輯,那麼調用 numCount
函數如何得出 0
呢?num
在函數 return
時已經在內存中被銷燬了啊!
所以,在本例中 JavaScript
的基礎類型並不保存在棧中,而應該保存在堆中,供 numCount
函數使用。
那麼網上你們的結論就是錯的了?非也!接下來談談我對 JavaScript
變量存儲的理解。
既然在 JavaScript
中有閉包的問題,拋開棧(Stack
),僅用堆可否實現變量存儲?咱們來看一個特殊的例子:
function test () {
let num = 1;
let string = 'string';
let bool = true;
let obj = {
attr1: 1,
attr2: 'string',
attr3: true,
attr4: 'other'
}
return function log() {
console.log(num, string, bool, obj);
}
}
複製代碼
伴隨着 test
的調用,爲了保證變量不被銷燬,在堆中先生成一個對象就叫 Scope
吧,把變量做爲 Scope
的屬性給存起來。堆中的數據結構大體以下所示:
那麼,這樣就能解決閉包的問題了嗎?
固然能夠,因爲 Scope
對象是存儲在堆中,所以返回的 log
函數徹底能夠擁有 Scope
對象 的訪問。下圖是該段代碼在 Chrome
中的執行效果:
紅框部分,與上述一致,同時也反應出了以前說起的問題:例子中 JavaScript
的變量並無存在棧中,而是在堆裏,用一個特殊的對象(Scope
)保存。
那麼在 JavaScript
變量究竟是如何進程存儲的?這和變量的類型直接掛鉤,接下來就談談在 JavaScript
中變量的類型。
在 JavaScript
中,變量分爲三種類型:
局部變量很好理解:在函數中聲明,且在函數返回後不會被其餘做用域所使用的對象。下面代碼中的 local*
都是局部變量。
function test () {
let local1 = 1;
var local2 = 'str';
const local3 = true;
let local4 = {a: 1};
return;
}
複製代碼
被捕獲變量就是局部變量的反面:在函數中聲明,但在函數返回後仍有未執行做用域(函數或是類)使用到該變量,那麼該變量就是被捕獲變量。下面代碼中的 catch*
都是被捕獲變量。
function test1 () {
let catch1 = 1;
var catch2 = 'str';
const catch3 = true;
let catch4 = {a: 1};
return function () {
console.log(catch1, catch2, catch3, catch4)
}
}
function test2 () {
let catch1 = 1;
let catch2 = 'str';
let catch3 = true;
var catch4 = {a: 1};
return class {
constructor(){
console.log(catch1, catch2, catch3, catch4)
}
}
}
console.dir(test1())
console.dir(test2())
複製代碼
複製代碼到 Chrome
便可查看輸出對象下的 [[Scopes]]
下有對應的 Scope
。
全局變量就是 global
,在 瀏覽器上爲 window
在 node
裏爲 global
。全局變量會被默認添加到函數做用域鏈的最低端,也就是上述函數中 [[Scopes]]
中的最後一個。
全局變量須要特別注意一點:var
和 let/const
的區別。
全局的 var
變量其實僅僅是爲 global
對象添加了一條屬性。
var testVar = 1;
// 與下述代碼一致
windows.testVar = 1;
複製代碼
全局的 let/const
變量不會修改 windows
對象,而是將變量的聲明放在了一個特殊的對象下(與 Scope
相似)。
let testLet = 1;
console.dir(() => {})
複製代碼
複製到 Chrome
有如下結果:
那麼變量的類型肯定了,如何進行存儲呢?有兩種:
Stack
)Heap
)相信看到這裏,你們內心應該都清楚了:除了局部變量,其餘的全都存在堆中!
但這是理想狀況,再問你們一個問題:JavaScript
解析器如何判斷一個變量是局部變量呢?
判斷出是否被內部函數引用便可!
那若是 JavaScript
解析器並無判斷呢?那就只能存在堆裏!
那麼你必定想問,Chrome
的 V8
可否判斷出,從結果看應該是能夠的。
紅框內僅有變量 a
,而變量 b
已經消失不見了。因爲 FireFox
打印不出 [[Scopes]]
屬性,所以,筆者判斷不出。固然,若是有大佬能深刻了解並補充的話,感激涕零。
好,瞭解瞭如何存儲,接下來咱們看看如何賦值。
其實不論變量是存在棧內,仍是存在堆裏(反正都是在內存裏),其結構和存值方式是差很少的,都有以下的結構:
那好如今咱們來看看賦值,根據 =
號右邊變量的類型分爲兩種方式:
何爲常量?常量就是一聲明就能夠肯定的值,好比 1
、"string"
、true
、{a: 1}
,都是常量,這些值一旦聲明就不可改變,有些人可能會犟,對象類型的這麼多是常量,它能夠改變啊,這個問題先留着,等下在解釋。
假設如今有以下代碼:
let foo = 1;
複製代碼
JavaScript
聲明瞭一個變量 foo
,且讓它的值爲 1
,內存中就會發生以下變化
若是如今又聲明瞭一個 bar
變量:
let bar = 2;
複製代碼
那麼內存中就會變成這樣:
如今回顧下剛剛的問題:對象類型算常量嗎?
好比有如下代碼:
let obj = {
foo: 1,
bar: 2
}
複製代碼
內存模型以下:
經過該圖,咱們就能夠知道,其實 obj
指向的內存地址保存的也是一個地址值,那好,若是咱們讓 obj.foo = 'foo'
其實修改的是 0x1021
所在的內存區域,但 obj
指向的內存地址不會發生改變,所以,對象是常量!
何爲變量?在上述過程當中的 foo
、bar
、obj
,都是變量,變量表明一種引用關係,其自己的值並不肯定。
那麼若是我將一個變量的值賦值給另外一變量,會發生什麼?
let x = foo;
複製代碼
如上圖所示,僅僅是將 x
引用到與 foo
同樣的地址值而已,並不會使用新的內存空間。
OK
賦值到此爲止,接下來是修改。
與變量賦值同樣,變量的修改也須要根據 =
號右邊變量的類型分爲兩種方式:
foo = 'foo';
複製代碼
如上圖所示,內存中保存了 'foo'
並將 foo
的引用地址修改成 0x0204
。
foo = bar;
複製代碼
如上圖所示,僅僅是將 foo
引用的地址修改了而已。
const
爲 ES6
新出的變量聲明的一種方式,被 const
修飾的變量不能改變。
其實對應到 JavaScript
的變量儲存圖中,就是變量所指向的內存地址不能發生變化。也就是那個箭頭不能有改變。
好比說如下代碼:
const foo = 'foo';
foo = 'bar'; // Error
複製代碼
如上圖的關係圖所示,foo
不能引用到別的地址值。
那好如今是否能解決你對下面代碼的困惑:
const obj = {
foo: 1,
bar: 2
};
obj.foo = 2;
複製代碼
其 obj
所引用的地址並無發生變化,發生變的部分爲另外一區域。以下圖所示
OK
進入一個面試時極度容易問到的問題:
let obj1 = {
foo: 'foo',
bar: 'bar'
}
let obj2 = obj1;
let boj3 = {
foo: 'foo',
bar: 'bar'
}
console.log(obj1 === obj2);
console.log(obj1 === obj3);
obj2.foo = 'foofoo';
console.log(obj1.foo === 'foofoo');
複製代碼
請依次說出 console
的結果。
咱們不討論結果,先看看內存中的結構。
因此你如今知道答案了嗎?
在 JavaScript
中變量並不是完徹底全的存在在棧中,早期的 JavaScript
編譯器甚至把全部的變量都存在一個名爲閉包的對象中,JavaScript
是一門以函數爲基礎的語言,其中的函數變化多端,所以使用棧並不能解決語言方面的問題,但願你們能看到,並真正的瞭解 JavaScript
在內存中的模型吧。
按照慣例,提幾個問題:
JavaScript
變量的類型都有哪些?JavaScript
對於基礎類型和對象類型是如何存儲的?該系列全部問題由 minimo
提出,愛你喲~~~