JS 變量存儲?棧 & 堆?NONONO!

前言

此次的 why what or how 主題:JavaScript 變量存儲。javascript

不管那門語言,變量是組成一切的基礎,一個數字是一個變量,一個對象也是一個變量,在 JavaScript 中甚至連一個函數都是一個變量。html

那麼如此重要的變量,在 JavaScript 中到底是如何進行存儲的?java

棧 & 堆 ?

棧(Stack)又名堆棧,它是一種運算受限的線性表。限定僅在表尾進行插入和刪除操做的線性表。node

在百度上搜索 JavaScript 變量存儲,能看到不少文章,無外乎一個結論:程序員

對於原始類型,數據自己是存在棧內,對於對象類型,在棧中存的只是一個堆內地址的引用。面試

可是,我忽然想到一個問題:若是說原始類型存在在棧中,那麼 JavaScript 中的閉包是如何實現的?windows

固然想要深究這個問題,有必要先把棧(Stack)和堆(Heap)給說說清楚。瀏覽器

那好,先說說棧。數據結構

棧是內存中一塊用於存儲局部變量和函數參數的線性結構,遵循着先進後出的原則。數據只能順序的入棧,順序的出棧。固然,棧只是內存中一片連續區域一種形式化的描述,數據入棧和出棧的操做僅僅是棧指針在內存地址上的上下移動而已。以下圖所示(以 C 語言爲例):閉包

變量在棧中存儲

如圖所示,棧指針剛開始指向內存中 0x001 的位置,接着 sum 函數開始調用,因爲聲明瞭兩個變量,往棧中存放了兩個數值,棧指針也對應開始移動,當 sum 函數調用結束時,僅僅是把棧指針往下移動而已,並非真正的數據彈出,數據還在,只不過下次賦值時會被覆蓋。

挺簡單的不是麼,但須要註明一點的是:內存中棧區的數據,在函數調用結束後,就會自動的出棧,不須要程序進行操做,操做系統會自動執行,換句話說:棧中的變量在函數調用結束後,就會消失。

所以棧的特色:輕量,不須要手動管理,函數調時建立,調用結束則消失。

堆能夠簡單的認爲是一大塊內存空間,就像一個籃子,你往裏面放什麼都不要緊,可是籃子是私人物品,操做系統並不會管你的籃子裏都放了什麼,也不會主動去清理你的籃子,所以在 C 語言中,堆中內容是須要程序員手動清理的,否則就會出現內存溢出的狀況。

爲了必定程度的解決堆的問題,一些高級語言(如 JAVA)提出了一個概念:GCGarbage 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 保存變量

那麼,這樣就能解決閉包的問題了嗎?

固然能夠,因爲 Scope 對象是存儲在堆中,所以返回的 log 函數徹底能夠擁有 Scope 對象 的訪問。下圖是該段代碼在 Chrome 中的執行效果:

Chrome 中 Scope 的表示

紅框部分,與上述一致,同時也反應出了以前說起的問題:例子中 JavaScript 的變量並無存在棧中,而是在堆裏,用一個特殊的對象(Scope)保存。

那麼在 JavaScript 變量究竟是如何進程存儲的?這和變量的類型直接掛鉤,接下來就談談在 JavaScript 中變量的類型。

三種類型

JavaScript 中,變量分爲三種類型:

  1. 局部變量
  2. 被捕獲變量
  3. 全局變量

局部變量

局部變量很好理解:在函數中聲明,且在函數返回後不會被其餘做用域所使用的對象。下面代碼中的 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,在 瀏覽器上爲 windownode 裏爲 global。全局變量會被默認添加到函數做用域鏈的最低端,也就是上述函數中 [[Scopes]] 中的最後一個。

全局變量須要特別注意一點:varlet/const 的區別。

var

全局的 var 變量其實僅僅是爲 global 對象添加了一條屬性。

var testVar = 1;

// 與下述代碼一致
windows.testVar = 1;
複製代碼

let / const

全局的 let/const 變量不會修改 windows 對象,而是將變量的聲明放在了一個特殊的對象下(與 Scope 相似)。

let testLet = 1;

console.dir(() => {})
複製代碼

複製到 Chrome 有如下結果:

let/const 全局變量

兩種方式

那麼變量的類型肯定了,如何進行存儲呢?有兩種:

  1. 棧(Stack
  2. 堆(Heap

相信看到這裏,你們內心應該都清楚了:除了局部變量,其餘的全都存在堆中!

但這是理想狀況,再問你們一個問題:JavaScript 解析器如何判斷一個變量是局部變量呢?

判斷出是否被內部函數引用便可!

那若是 JavaScript 解析器並無判斷呢?那就只能存在堆裏!

那麼你必定想問,ChromeV8 可否判斷出,從結果看應該是能夠的。

Chrome 下的局部變量

紅框內僅有變量 a,而變量 b 已經消失不見了。因爲 FireFox 打印不出 [[Scopes]] 屬性,所以,筆者判斷不出。固然,若是有大佬能深刻了解並補充的話,感激涕零。

好,瞭解瞭如何存儲,接下來咱們看看如何賦值。

變量賦值

其實不論變量是存在棧內,仍是存在堆裏(反正都是在內存裏),其結構和存值方式是差很少的,都有以下的結構:

變量存儲

那好如今咱們來看看賦值,根據 = 號右邊變量的類型分爲兩種方式:

賦值爲常量

何爲常量?常量就是一聲明就能夠肯定的值,好比 1"string"true{a: 1},都是常量,這些值一旦聲明就不可改變,有些人可能會犟,對象類型的這麼多是常量,它能夠改變啊,這個問題先留着,等下在解釋。

假設如今有以下代碼:

let foo = 1;
複製代碼

JavaScript 聲明瞭一個變量 foo,且讓它的值爲 1,內存中就會發生以下變化

常量儲存

若是如今又聲明瞭一個 bar 變量:

let bar = 2;
複製代碼

那麼內存中就會變成這樣:

foo & bar

如今回顧下剛剛的問題:對象類型算常量嗎?

好比有如下代碼:

let obj = {
    foo: 1,
    bar: 2
}
複製代碼

內存模型以下:

JavaScript Object存儲

經過該圖,咱們就能夠知道,其實 obj 指向的內存地址保存的也是一個地址值,那好,若是咱們讓 obj.foo = 'foo' 其實修改的是 0x1021 所在的內存區域,但 obj 指向的內存地址不會發生改變,所以,對象是常量!

賦值爲變量

何爲變量?在上述過程當中的 foobarobj,都是變量,變量表明一種引用關係,其自己的值並不肯定。

那麼若是我將一個變量的值賦值給另外一變量,會發生什麼?

let x = foo;
複製代碼

x 賦值爲 foo 變量

如上圖所示,僅僅是將 x 引用到與 foo 同樣的地址值而已,並不會使用新的內存空間。

OK 賦值到此爲止,接下來是修改。

變量修改

與變量賦值同樣,變量的修改也須要根據 = 號右邊變量的類型分爲兩種方式:

修改成常量

foo = 'foo';
複製代碼

foo 變量修改成另外一常量

如上圖所示,內存中保存了 'foo' 並將 foo 的引用地址修改成 0x0204

修改成變量

foo = bar;
複製代碼

foo 變量修改成另外一變量

如上圖所示,僅僅是將 foo 引用的地址修改了而已。

const 的工做機制

constES6 新出的變量聲明的一種方式,被 const 修飾的變量不能改變。

其實對應到 JavaScript 的變量儲存圖中,就是變量所指向的內存地址不能發生變化。也就是那個箭頭不能有改變。

好比說如下代碼:

const foo = 'foo';
foo = 'bar'; // Error
複製代碼

const 不容許從新賦值

如上圖的關係圖所示,foo 不能引用到別的地址值。

那好如今是否能解決你對下面代碼的困惑:

const obj = {
    foo: 1,
    bar: 2
};
obj.foo = 2;
複製代碼

obj 所引用的地址並無發生變化,發生變的部分爲另外一區域。以下圖所示

const 對象類型修改

對象的修改

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 的結果。

咱們不討論結果,先看看內存中的結構。

js 中對象存儲

因此你如今知道答案了嗎?

總結

JavaScript 中變量並不是完徹底全的存在在棧中,早期的 JavaScript 編譯器甚至把全部的變量都存在一個名爲閉包的對象中,JavaScript 是一門以函數爲基礎的語言,其中的函數變化多端,所以使用棧並不能解決語言方面的問題,但願你們能看到,並真正的瞭解 JavaScript 在內存中的模型吧。

按照慣例,提幾個問題:

  1. JavaScript 變量的類型都有哪些?
  2. JavaScript 對於基礎類型和對象類型是如何存儲的?

參考

最後的最後

該系列全部問題由 minimo 提出,愛你喲~~~

相關文章
相關標籤/搜索