【準備面試】-JS - 調用堆棧

這期徹底復刻木易老師的博客,由於寫的很好 我這裏只是本身複習,沒有商用node

木易老師博客地址git

執行上下文

執行上下文總共有三種類型:github

全局執行上下文:只有一個,瀏覽器中的全局對象就是 window 對象,this 指向這個全局對象。算法

函數執行上下文:存在無數個,只有在函數被調用的時候纔會被建立,每次調用函數都會建立一個新的執行上下文。數組

Eval 函數執行上下文: 指的是運行在 eval 函數中的代碼,不多用並且不建議使用。瀏覽器

執行棧

執行棧,也叫調用棧,具備 LIFO(後進先出)結構,用於存儲在代碼執行期間建立的全部執行上下文。bash

首次運行JS代碼時,會建立一個全局執行上下文並Push到當前的執行棧中。每當發生函數調用,引擎都會爲該函數建立一個新的函數執行上下文並Push到當前執行棧的棧頂。服務器

根據執行棧LIFO規則,當棧頂函數運行完成後,其對應的函數執行上下文將會從執行棧中Pop出,上下文控制權將移到當前執行棧的下一個執行上下文。數據結構

function first() {  
  console.log('Inside first function');  
  second();  
  console.log('Again inside first function');  
}

function second() {  
  console.log('Inside second function');  
}

first();  
console.log('Inside Global Execution Context');

// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context
複製代碼

執行上下文的建立

執行上下文分兩個階段建立:1)建立階段; 2)執行階段閉包

建立階段

一、肯定 this 的值,也被稱爲 This Binding。

二、LexicalEnvironment(詞法環境) 組件被建立。

三、VariableEnvironment(變量環境) 組件被建立。 直接看僞代碼可能更加直觀

ExecutionContext = {
ThisBinding = , // 肯定this LexicalEnvironment = { ... }, // 詞法環境 VariableEnvironment = { ... }, // 變量環境 } This Binding 全局執行上下文中,this 的值指向全局對象,在瀏覽器中this 的值指向 window 對象,而在nodejs中指向這個文件的module對象。

函數執行上下文中,this 的值取決於函數的調用方式。具體有:默認綁定、隱式綁定、顯式綁定(硬綁定)、new綁定、箭頭函數,具體內容會在【this全面解析】部分詳解。

詞法環境(Lexical Environment) 詞法環境有兩個組成部分

  • 一、環境記錄:存儲變量和函數聲明的實際位置

  • 二、對外部環境的引用:能夠訪問其外部詞法環境

詞法環境有兩種類型

  • 一、全局環境:是一個沒有外部環境的詞法環境,其外部環境引用爲 null。擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,this 的值指向這個全局對象。

  • 二、函數環境:用戶在函數中定義的變量被存儲在環境記錄中,包含了arguments 對象。對外部環境的引用能夠是全局環境,也能夠是包含內部函數的外部函數環境。

直接看僞代碼可能更加直觀

GlobalExectionContext = {  // 全局執行上下文
  LexicalEnvironment: {    	  // 詞法環境
    EnvironmentRecord: {   		// 環境記錄
      Type: "Object",      		   // 全局環境
      // 標識符綁定在這裏 
      outer: <null>  	   		   // 對外部環境的引用
  }  
}

FunctionExectionContext = { // 函數執行上下文
  LexicalEnvironment: {  	  // 詞法環境
    EnvironmentRecord: {  		// 環境記錄
      Type: "Declarative",  	   // 函數環境
      // 標識符綁定在這裏 			  // 對外部環境的引用
      outer: <Global or outer function environment reference>  
  }  
}
複製代碼

變量環境 變量環境也是一個詞法環境,所以它具備上面定義的詞法環境的全部屬性。

在 ES6 中,詞法 環境和 變量 環境的區別在於前者用於存儲函數聲明和變量( let 和 const )綁定,然後者僅用於存儲變量( var )綁定。

使用例子進行介紹

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);
複製代碼

執行上下文以下所示

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 標識符綁定在這裏  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 標識符綁定在這裏  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  
   
  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 標識符綁定在這裏  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 標識符綁定在這裏  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}
複製代碼

提高

  • 1.變量提高
console.log(num); //undefined
var num = 1;
複製代碼
  • 2.函數提高
foo();  // foo2
var foo = function() {
    console.log('foo1');
}

foo();  // foo1,foo從新賦值

function foo() {
    console.log('foo2');
}

foo(); // foo1
複製代碼

執行上下文棧

由於JS引擎建立了不少的執行上下文,因此JS引擎建立了執行上下文棧(Execution context stack,ECS)來管理執行上下文。

有以下兩段代碼,執行的結果是同樣的,可是兩段代碼究竟有什麼不一樣?

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
複製代碼
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
複製代碼

答案是 執行上下文棧的變化不同。

第一段代碼:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
複製代碼

第二段代碼:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
複製代碼

函數上下文

在函數上下文中,用活動對象(activation object, AO)來表示變量對象。

活動對象和變量對象的區別在於

  • 一、變量對象(VO)是規範上或者是JS引擎上實現的,並不能在JS環境中直接訪問。
  • 二、當進入到一個執行上下文後,這個變量對象纔會被激活,因此叫活動對象(AO),這時候活動對象上的各類屬性才能被訪問。 調用函數時,會爲其建立一個Arguments對象,並自動初始化局部變量arguments,指代該Arguments對象。全部做爲參數傳入的值都會成爲Arguments對象的數組元素。

執行過程 執行上下文的代碼會分紅兩個階段進行處理

  • 一、進入執行上下文

  • 二、代碼執行

進入執行上下文 很明顯,這個時候尚未執行代碼

此時的變量對象會包括(以下順序初始化):

  • 一、函數的全部形參 (only函數上下文):沒有實參,屬性值設爲undefined。
  • 二、函數聲明:若是變量對象已經存在相同名稱的屬性,則徹底替換這個屬性。
  • 三、變量聲明:若是變量名稱跟已經聲明的形參或函數相同,則變量聲明不會干擾已經存在的這類屬性。 上代碼就直觀了
function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;
}

foo(1);
複製代碼

對於上面的代碼,這個時候的AO是

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}
複製代碼

形參arguments這時候已經有賦值了,可是變量仍是undefined,只是初始化的值

代碼執行 這個階段會順序執行代碼,修改變量對象的值,執行完成後AO以下

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}
複製代碼

總結以下:

  • 一、全局上下文的變量對象初始化是全局對象

  • 二、函數上下文的變量對象初始化只包括 Arguments 對象

  • 三、在進入執行上下文時會給變量對象添加形參、函數聲明、變量聲明等初始的屬性值

  • 四、在代碼執行階段,會再次修改變量對象的屬性值

內存

  • 1.基本類型:--> 棧內存(不包含閉包中的變量)
  • 2.引用類型:--> 堆內存

內存回收

JavaScript有自動垃圾收集機制,垃圾收集器會每隔一段時間就執行一次釋放操做,找出那些再也不繼續使用的值,而後釋放其佔用的內存。

局部變量和全局變量的銷燬

  • 局部變量:局部做用域中,當函數執行完畢,局部變量也就沒有存在的必要了,所以垃圾收集器很容易作出判斷並回收。
  • 全局變量:全局變量何時須要自動釋放內存空間則很難判斷,因此在開發中儘可能避免使用全局變量。

以Google的V8引擎爲例,V8引擎中全部的JS對象都是經過來進行內存分配的

  • 初始分配:當聲明變量並賦值時,V8引擎就會在堆內存中分配給這個變量。
  • 繼續申請:當已申請的內存不足以存儲這個變量時,V8引擎就會繼續申請內存,直到堆的大小達到了V8引擎的內存上限爲止。

V8引擎對堆內存中的JS對象進行分代管理

  • 新生代:存活週期較短的JS對象,如臨時變量、字符串等。
  • 老生代:通過屢次垃圾回收仍然存活,存活週期較長的對象,如主控制器、服務器對象等。

垃圾回收算法

對垃圾回收算法來講,核心思想就是如何判斷內存已經再也不使用,經常使用垃圾回收算法有下面兩種。

  • 1.引用計數(現代瀏覽器再也不使用)
  • 2.標記清除(經常使用)

引用計數

引用計數算法定義「內存再也不使用」的標準很簡單,就是看一個對象是否有指向它的引用。若是沒有其餘對象指向它了,說明該對象已經再也不須要了。

// 建立一個對象person,他有兩個指向屬性age和name的引用
var person = {
    age: 12,
    name: 'aaaa'
};

person.name = null; // 雖然name設置爲null,但由於person對象還有指向name的引用,所以name不會回收

var p = person; 
person = 1;         //原來的person對象被賦值爲1,但由於有新引用p指向原person對象,所以它不會被回收

p = null;           //原person對象已經沒有引用,很快會被回收
複製代碼

引用計數有一個致命的問題,那就是循環引用

若是兩個對象相互引用,儘管他們已再也不使用,可是垃圾回收器不會進行回收,最終可能會致使內存泄露。

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 

    return "cycle reference!"
}

cycle();
複製代碼

cycle函數執行完成以後,對象o1和o2實際上已經再也不須要了,但根據引用計數的原則,他們之間的相互引用依然存在,所以這部份內存不會被回收。因此現代瀏覽器再也不使用這個算法。

可是IE依舊使用。

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};
複製代碼

上面的寫法很常見,可是上面的例子就是一個循環引用。

變量div有事件處理函數的引用,同時事件處理函數也有div的引用,由於div變量可在函數內被訪問,因此循環引用就出現了。

標記清除(經常使用)

標記清除算法將「再也不使用的對象」定義爲「沒法到達的對象」。即從根部(在JS中就是全局對象)出發定時掃描內存中的對象,凡是能從根部到達的對象,保留。那些從根部出發沒法觸及到的對象被標記爲再也不使用,稍後進行回收。沒法觸及的對象包含了沒有引用的對象這個概念,但反之未必成立。因此上面的例子就能夠正確被垃圾回收處理了。

算法由如下幾步組成:

  • 一、垃圾回收器建立了一個「roots」列表。roots 一般是代碼中全局變量的引用。JavaScript 中,「window」 對象是一個全局變量,被看成 root 。window 對象老是存在,所以垃圾回收器能夠檢查它和它的全部子對象是否存在(即不是垃圾);

  • 二、全部的 roots 被檢查和標記爲激活(即不是垃圾)。全部的子對象也被遞歸地檢查。從 root 開始的全部對象若是是可達的,它就不被看成垃圾。

  • 三、全部未被標記的內存會被當作垃圾,收集器如今能夠釋放內存,歸還給操做系統了。

現代的垃圾回收器改良了算法,可是本質是相同的:可達內存被標記,其他的被看成垃圾回收。

內存泄漏

對於持續運行的服務進程(daemon),必須及時釋放再也不用到的內存。不然,內存佔用愈來愈高,輕則影響系統性能,重則致使進程崩潰。 對於再也不用到的內存,沒有及時釋放,就叫作內存泄漏(memory leak) 內存泄漏識別方法

  • 一、瀏覽器方法 打開開發者工具,選擇 Memory 在右側的Select profiling type字段裏面勾選 timeline 點擊左上角的錄製按鈕。 在頁面上進行各類操做,模擬用戶的使用狀況。 一段時間後,點擊左上角的 stop 按鈕,面板上就會顯示這段時間的內存佔用狀況。
  • 二、命令行方法 使用 Node 提供的 process.memoryUsage 方法。
console.log(process.memoryUsage());

// 輸出
{ 
  rss: 27709440,		// resident set size,全部內存佔用,包括指令區和堆棧
  heapTotal: 5685248,   // "堆"佔用的內存,包括用到的和沒用到的
  heapUsed: 3449392,	// 用到的堆的部分
  external: 8772 		// V8 引擎內部的 C++ 對象佔用的內存
}
複製代碼

判斷內存泄漏,以heapUsed字段爲準。

WeakMap

ES6 新出的兩種數據結構:WeakSetWeakMap,表示這是弱引用,它們對於值的引用都是不計入垃圾回收機制的。

const wm = new WeakMap();
const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"
複製代碼

先新建一個 Weakmap 實例,而後將一個 DOM 節點做爲鍵名存入該實例,並將一些附加信息做爲鍵值,一塊兒存放在 WeakMap 裏面。這時,WeakMap 裏面對element的引用就是弱引用,不會被計入垃圾回收機制。

四種常見的JS內存泄漏

一、意外的全局變量

未定義的變量會在全局對象建立一個新變量,以下。

function foo(arg) {
    bar = "this is a hidden global variable";
}
函數 foo 內部忘記使用 var ,實際上JS會把bar掛載到全局對象上,意外建立一個全局變量。

function foo(arg) {
    window.bar = "this is an explicit global variable";
}
另外一個意外的全局變量可能由 this 建立。

function foo() {
    this.variable = "potential accidental global";
}

// Foo 調用本身,this 指向了全局對象(window)
// 而不是 undefined
foo();
複製代碼

解決方法:在 JavaScript 文件頭部加上 'use strict',使用嚴格模式避免意外的全局變量,此時上例中的this指向undefined。若是必須使用全局變量存儲大量數據時,確保用完之後把它設置爲 null 或者從新定義。

二、被遺忘的計時器或回調函數

計時器setInterval代碼很常見

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 處理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
複製代碼

上面的例子代表,在節點node或者數據再也不須要時,定時器依舊指向這些數據。因此哪怕當node節點被移除後,interval 仍舊存活而且垃圾回收器沒辦法回收,它的依賴也沒辦法被回收,除非終止定時器。

var element = document.getElementById('button');
function onClick(event) {
    element.innerHTML = 'text';
}

element.addEventListener('click', onClick);
複製代碼

對於上面觀察者的例子,一旦它們再也不須要(或者關聯的對象變成不可達),明確地移除它們很是重要。老的 IE 6 是沒法處理循環引用的。由於老版本的 IE 是沒法檢測 DOM 節點與 JavaScript 代碼之間的循環引用,會致使內存泄漏。 可是,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法(標記清除),已經能夠正確檢測和處理循環引用了。即回收節點內存時,沒必要非要調用removeEventListener了。

三、脫離 DOM 的引用

若是把DOM 存成字典(JSON 鍵值對)或者數組,此時,一樣的 DOM 元素存在兩個引用:一個在 DOM樹中,另外一個在字典中。那麼未來須要把兩個引用都清除。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
    // 更多邏輯
}
function removeButton() {
    // 按鈕是 body 的後代元素
    document.body.removeChild(document.getElementById('button'));
    // 此時,仍舊存在一個全局的 #button 的引用
    // elements 字典。button 元素仍舊在內存中,不能被 GC 回收。
}
複製代碼

若是代碼中保存了表格某一個<td> 的引用。未來決定刪除整個表格的時候,直覺認爲 GC 會回收除了已保存的<td>之外的其它節點。實際狀況並不是如此:此 <td> 是表格的子節點,子元素與父元素是引用關係。因爲代碼保留了<td> 的引用,致使整個表格仍待在內存中。因此保存 DOM 元素引用的時候,要當心謹慎。

4.閉包

閉包的關鍵是匿名函數能夠訪問父級做用域的變量。

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
    
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};

setInterval(replaceThing, 1000);
複製代碼

每次調用 replaceThing ,theThing 獲得一個包含一個大數組和一個新閉包(someMethod)的新對象。同時,變量 unused 是一個引用 originalThing 的閉包(先前的 replaceThing 又調用了 theThing )。someMethod 能夠經過 theThing 使用,someMethod 與 unused 分享閉包做用域,儘管 unused 從未使用,它引用的 originalThing 迫使它保留在內存中(防止被回收)。

解決方法:在 replaceThing 的最後添加 originalThing = null 。

相關文章
相關標籤/搜索