【瀏覽器】(內附面試題)瀏覽器中堆棧內存的底層處理機制

寫在前面

咱們寫的JS代碼瀏覽器是如何解析的?瀏覽器在執行JS代碼的過程當中發生了什麼?…這些問題可能在完成業務功能代碼的過程當中並無多麼重要,可是做爲一名前端開發工程師,瞭解這些會提高本身的思惟能力、會讓本身更深層次的理解JavaScript語言。這篇文章將詳細闡述瀏覽器中堆棧內存的底層處理機制、垃圾回收機制以及內存泄漏的幾種狀況。javascript

瀏覽器執行代碼須要經歷什麼

編譯

  • 詞法解析:這個過程會將由字符組成的字符串分解成有意義的代碼塊(詞法單元)。前端

  • 語法分析:這個過程將詞法單元流轉換成一個由元素逐級嵌套所組成的表明了程序語法的樹(抽象語法樹 => AST)。java

  • 代碼生成:將AST轉換爲可執行代碼的過程被稱爲代碼生成。web

引擎編譯執行代碼

而後將構建出的代碼交給引擎(V8),這個時候可能會遇到變量提高做用域和做用域鏈/閉包變量對象堆棧內存GO/VO/AO/EC/ECStack、…。面試

引擎在編譯執行代碼的過程當中,首先會建立一個執行棧,也就是棧內存(ECStack => 執行環境棧),而後執行代碼。瀏覽器

在代碼執行會建立EC(執行上下文),執行上下文分爲全局執行上下文(EC(G))和函數執行上下文(EC(...)),其中函數的執行上下文是私有的。閉包

建立執行上下文的過程當中,可能會建立:app

  • GO(Global Object):全局對象 瀏覽器端,會把GO賦值給window
  • VO(Varible Object):變量對象,存儲當前上下文中的變量。
  • AO(Active Object):活動對象

而後將進棧執行,建立好的上下文將壓縮到棧中執行,執行後一些沒用的上下文將出棧,有用的上下文會壓縮到棧底(閉包)。棧底永遠是全局執行上下文,棧頂則永遠是當前執行上下文。dom

下面的一張圖表達了整個流程函數

變量賦值的三步操做

第一步,建立變量,這個過程叫作聲明(declare)。

第二步,建立值。基本類型值會直接在棧中建立和存儲;因爲引用類型值是複雜的結構,因此需開闢一個存儲對象中鍵值對(存儲函數中代碼)的內存空間,這個內存就是堆內存,全部的堆內存都有可被後續查找的16進制地址,後續關聯賦值時,是把堆內存地址給予變量操做。

最後一步,將變量和值關聯,這個過程叫作定義(defined)。這裏,若是值只通過了聲明,而沒有進行賦值操做,這個值就是未定義(undefined)。

一道題理解這個過程

我將以畫圖的形式展現

// 1.
let a = 12;
let b = a;
b = 13;
console.log(a);

// 2.
let a = {n:12};
let b = a;
b['n'] = 13;
console.log(a.n);

// 3.
let a = {n:12};
let b = a;
b = {n:13};
console.log(a.n);
複製代碼
  • 第一問

建立執行棧,造成全局執行上下文,而且建立GO,進入棧中執行代碼

基本類型直接在棧中建立和存儲

因此本問最終輸出的a值爲12

  • 第二問

建立執行棧,造成全局執行上下文,而且建立GO,進入棧中執行代碼

引用類型值比較複雜,將建立堆內存

因此本問最終輸出的a.n13

  • 第三問

建立執行棧,造成全局執行上下文,而且建立GO,進入棧中執行代碼

引用類型值比較複雜,將建立堆內存

因此本問最終輸出的a.n的值爲12

幾道面試題讓你更深次理解瀏覽器堆棧內存的底層處理機制

  • 第一個題
let a = {
    n10
};
let b = a;
b.m = b = {
    n20
};
console.log(a);
console.log(b);
複製代碼

建立執行棧,造成全局執行上下文,而且建立GO,進入棧中執行代碼

引用類型值比較複雜,將建立堆內存

因此最終輸出的a{n: 10, m: {n: 20}};b{n: 20}

  • 第二個題:
let x = [1223];
function fn(y{
    y[0] = 100;
    y = [100];
    y[1] = 200;
    console.log(y);
}
fn(x);
console.log(x);
複製代碼

首先會建立ECStack,造成全局執行上下文,建立VO(變量對象), 而後進入棧中執行代碼

變量賦值

接下來會執行fn(x)函數,函數執行會造成一個全新的執行上下文,會產生AO。上面說過,棧頂永遠是當前執行上下文,棧底是全局執行上下文,因此函數執行,函數執行上下文將進棧,會將全局執行上下文壓入棧底。

而後進行代碼的執行操做,執行後會出棧

繼續執行,打印出x,通過上述分析:

答案是:[100, 200] [100, 23]

  • 第三個題
var x = 10;
function (x{
    console.log(x);
    x = x || 20 && 30 || 40;
    console.log(x);
}();
console.log(x);
複製代碼

因此,最終的結果爲:undefined 30 10

  • 第四題
let x = [12],
    y = [34];
function (x{
    x.push('A');
    x = x.slice(0);
    x.push('B');
    x = y;
    x.push('C');
    console.log(x, y);
}(x);
console.log(x, y);
複製代碼

因此本題最終的輸出結果爲[3, 4, 'C'] [3, 4, 'C'] [1, 2, 'A'] [3, 4, 'C']

垃圾回收機制

瀏覽器的Javascript具備自動垃圾回收機制(GC:Garbage Collecation),垃圾收集器會按期(週期性)找出那些不在繼續使用的變量,而後釋放其內存。

標記清除

js中,最經常使用的垃圾回收機制是標記清除:當變量進入執行環境時,被標記爲「進入環境」,當變量離開執行環境時,會被標記爲「離開環境」。垃圾回收器會銷燬那些帶標記的值並回收它們所佔用的內存空間。

function demo({
    var a = 1;     // 標記"進入環境"
    var b = 2;     // 標記"進入環境"


demo();            // 函數執行完畢,a和b標記爲"離開環境"
複製代碼

引用計數

瀏覽器會跟蹤記錄值的引用次數,每多引用一次,引用次數就會加1,取消佔用,引用次數就會減1,當引用次數爲0時,瀏覽器會進行垃圾回收。

下面的例子說明a引用次數的變化

function demo({
    var a = {};     // +1
    var b = a;      // +1 => 2
    b = null;       // -1 => 1
    a = null;       // -1 => 0   ====> 此時會回收
}
複製代碼

內存泄漏

內存泄露是指程序中的某些函數或者任務執行完畢以後,本該釋放的內存空間,因爲各類緣由,沒有被釋放,致使程序愈來愈耗內存,最終可能引起程序崩潰等各類嚴重後果。

在 JS 中,常見的內存泄露主要有 4 種

全局變量

一個例子直接說明:

 var obj = null
 function foo(){
      obj = { name:"小紅" }; 
 }
 foo();
複製代碼

上述代碼中,obj是一個全局變量,這樣,全部和obj做用域同層級的函數均可以訪問到obj對象,因此obj對象不會被回收。

閉包

閉包是 JS 中最容易引發內存泄露的特性

function foo(){
    var obj = {name:"小紅"}
    return function(){
         return obj.name;
    }
}
var func = foo();   // foo返回的值是一個函數,func也變成了一個外部函數
func();             // 外部函數func能狗訪問foo內部的user對象。
複製代碼

上述代碼中,foo函數執行完後,由於在func()中依然可以訪問到obj,因此變量obj沒有被釋放,這就致使了內存泄漏,咱們能夠用下面的方法解決

function foo(){
    var obj = {name:"小紅"}
    return function(){
        var obj1 = obj;
        obj = null;
        return obj1.name;
    }
}
var func = foo();   // foo返回的值是一個函數,func也變成了一個外部函數
func();          
複製代碼

上面的代碼中,在foo函數返回的函數中,及時將obj釋放了,這個時候,在func函數執行時,就不會訪問到局部變量obj了。

DOM 元素的引用

DOM元素的引用中,會出現內存泄漏

  • DOM元素刪除了,可是JS對象中的引用沒刪除
<body>
    <div id="app"></div>
    <script>
        var appDom = document.getElementById("app");

        appDom.onclick = function({
            document.body.removeChild(document.getElementById("app"));
        }
    
</script>
</body>
複製代碼

上面的例子中,點擊#app時,清除了該DOM節點,可是appDom依然保留對其的引用,致使#app沒有被釋放。

  • 使用第三方庫

定時器

setInterval函數的定時器會一直循環,除非手動清除,這就出現了內存隱患,因此咱們應該在使用完定時器時對定時器及時清除。

var count = setInterval(() => {
    console.log(1);
}, 1000);

// 使用完成
clearInterval(count)
複製代碼

以上是致使內存泄漏的四種狀況(例子不僅有文中的幾個,在平時的開發工做中還會有不少的例子),在咱們的平常開發工做中,應該避免這四種狀況的發生,因此咱們寫代碼的額過程當中,要多多注意。

總結

本文詳細講解了在瀏覽器中是如何對堆棧內存進行處理的,也簡單說了一下垃圾回收機制的幾種方法和形成內存泄漏的幾種狀況。還但願你們在仔細閱讀後(自動忽略掉我寫的醜字🐶),可以指出其中不合理甚至錯誤的地方,咱們共同窗習,共同進步~

最後,分享一下個人公衆號「web前端日記」,但願你們多多關注

相關文章
相關標籤/搜索