深刻理解 JavaScript 執行上下文和執行棧

Fundebug經受權轉載,版權歸原做者全部。javascript

前言

若是你是一名 JavaScript 開發者,或者想要成爲一名 JavaScript 開發者,那麼你必須知道 JavaScript 程序內部的執行機制。執行上下文和執行棧是 JavaScript 中關鍵概念之一,是 JavaScript 難點之一。 理解執行上下文和執行棧一樣有助於理解其餘的 JavaScript 概念如提高機制、做用域和閉包等。本文儘量用通俗易懂的方式來介紹這些概念。html

1、執行上下文(Execution Context)

1. 什麼是執行上下文

簡而言之,執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境的抽象概念, JavaScript 中運行任何的代碼都是在執行上下文中運行前端

2. 執行上下文的類型

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

  • 全局執行上下文: 這是默認的、最基礎的執行上下文。不在任何函數中的代碼都位於全局執行上下文中。它作了兩件事:1. 建立一個全局對象,在瀏覽器中這個全局對象就是 window 對象。2. 將 this 指針指向這個全局對象。一個程序中只能存在一個全局執行上下文。
  • 函數執行上下文: 每次調用函數時,都會爲該函數建立一個新的執行上下文。每一個函數都擁有本身的執行上下文,可是隻有在函數被調用的時候纔會被建立。一個程序中能夠存在任意數量的函數執行上下文。每當一個新的執行上下文被建立,它都會按照特定的順序執行一系列步驟,具體過程將在本文後面討論。
  • Eval 函數執行上下文: 運行在 eval 函數中的代碼也得到了本身的執行上下文,但因爲 Javascript 開發人員不經常使用 eval 函數,因此在這裏再也不討論。

2、執行上下文的生命週期

執行上下文的生命週期包括三個階段:建立階段 → 執行階段 → 回收階段,本文重點介紹建立階段。git

1. 建立階段

當函數被調用,但未執行任何其內部代碼以前,會作如下三件事:github

  • 建立變量對象:首先初始化函數的參數 arguments,提高函數聲明和變量聲明。下文會詳細說明。
  • 建立做用域鏈(Scope Chain):在執行期上下文的建立階段,做用域鏈是在變量對象以後建立的。做用域鏈自己包含變量對象。做用域鏈用於解析變量。當被要求解析變量時,JavaScript 始終從代碼嵌套的最內層開始,若是最內層沒有找到變量,就會跳轉到上一層父做用域中查找,直到找到該變量。
  • 肯定 this 指向:包括多種狀況,下文會詳細說明

在一段 JS 腳本執行以前,要先解析代碼(因此說 JS 是解釋執行的腳本語言),解析的時候會先建立一個全局執行上下文環境,先把代碼中即將執行的變量、函數聲明都拿出來。變量先暫時賦值爲 undefined,函數則先聲明好可以使用。這一步作完了,而後再開始正式執行程序。面試

另外,一個函數在執行以前,也會建立一個函數執行上下文環境,跟全局上下文差很少,不過 函數執行上下文中會多出 this arguments 和函數的參數。express

2. 執行階段

執行變量賦值、代碼執行編程

3. 回收階段

執行上下文出棧等待虛擬機回收執行上下文小程序

給你們推薦一個好用的 BUG 監控工具Fundebug,歡迎免費試用!

3、變量提高和 this 指向的細節

1. 變量聲明提高

大部分編程語言都是先聲明變量再使用,但在 JS 中,事情有些不同:

console.log(a); // undefined
var a = 10;

上述代碼正常輸出undefined而不是報錯Uncaught ReferenceError: a is not defined,這是由於聲明提高(hoisting),至關於以下代碼:

var a; //聲明 默認值是undefined 「準備工做」
console.log(a);
a = 10; //賦值

2. 函數聲明提高

咱們都知道,建立一個函數的方法有兩種,一種是經過函數聲明function foo(){} 另外一種是經過函數表達式var foo = function(){} ,那這兩種在函數提高有什麼區別呢?

console.log(f1); // function f1(){}
function f1() {} // 函數聲明
console.log(f2); // undefined
var f2 = function() {}; // 函數表達式

接下來咱們經過一個例子來講明這個問題:

function test() {
    foo(); // Uncaught TypeError "foo is not a function"
    bar(); // "this will run!"
    var foo = function() {
        // function expression assigned to local variable 'foo'
        alert("this won't run!");
    };
    function bar() {
        // function declaration, given the name 'bar'
        alert("this will run!");
    }
}
test();

在上面的例子中,foo()調用的時候報錯了,而 bar 可以正常調用。

咱們前面說過變量和函數都會上升,遇到函數表達式 var foo = function(){}時,首先會將var foo上升到函數體頂部,然而此時的 foo 的值爲 undefined,因此執行foo()報錯。

而對於函數bar(), 則是提高了整個函數,因此bar()纔可以順利執行。

有個細節必須注意:當遇到函數和變量同名且都會被提高的狀況,函數聲明優先級比較高,所以變量聲明會被函數聲明所覆蓋,可是能夠從新賦值。

alert(a); //輸出:function a(){ alert('我是函數') }
function a() {
    alert("我是函數");
} //
var a = "我是變量";
alert(a); //輸出:'我是變量'

function 聲明的優先級比 var 聲明高,也就意味着當兩個同名變量同時被 function 和 var 聲明時,function 聲明會覆蓋 var 聲明

這代碼等效於:

function a() {
    alert("我是函數");
}
var a; //hoisting
alert(a); //輸出:function a(){ alert('我是函數') }
a = "我是變量"; //賦值
alert(a); //輸出:'我是變量'

最後咱們看個複雜點的例子:

function test(arg) {
    // 1. 形參 arg 是 "hi"
    // 2. 由於函數聲明比變量聲明優先級高,因此此時 arg 是 function
    console.log(arg);
    var arg = "hello"; // 3.var arg 變量聲明被忽略, arg = 'hello'被執行
    function arg() {
        console.log("hello world");
    }
    console.log(arg);
}
test("hi");
/* 輸出:
function arg(){
    console.log('hello world') 
    }
hello 
*/

這是由於當函數執行的時候,首先會造成一個新的私有的做用域,而後依次按照以下的步驟執行:

  • 若是有形參,先給形參賦值
  • 進行私有做用域中的預解釋,函數聲明優先級比變量聲明高,最後後者會被前者所覆蓋,可是能夠從新賦值
  • 私有做用域中的代碼從上到下執行

3. 肯定 this 的指向

先搞明白一個很重要的概念 —— this 的值是在執行的時候才能確認,定義的時候不能確認! 爲何呢 —— 由於 this 是執行上下文環境的一部分,而執行上下文須要在代碼執行以前肯定,而不是定義的時候。看以下例子:

// 狀況1
function foo() {
  console.log(this.a) //1
}
var a = 1
foo()

// 狀況2
function fn(){
  console.log(this);
}
var obj={fn:fn};
obj.fn(); //this->obj

// 狀況3
function CreateJsPerson(name,age){
//this是當前類的一個實例p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
var p1=new CreateJsPerson("尹華芝",48);

// 狀況4
function add(c, d){
  return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

// 狀況5
<button id="btn1">箭頭函數this</button>
<script type="text/javascript">
    let btn1 = document.getElementById('btn1');
    let obj = {
        name: 'kobe',
        age: 39,
        getName: function () {
            btn1.onclick = () => {
                console.log(this);//obj
            };
        }
    };
    obj.getName();
</script>

接下來咱們逐一解釋上面幾種狀況

  • 對於直接調用 foo 來講,無論 foo 函數被放在了什麼地方,this 必定是 window
  • 對於 obj.foo() 來講,咱們只須要記住,誰調用了函數,誰就是 this,因此在這個場景下 foo 函數中的 this 就是 obj 對象
  • 在構造函數模式中,類中(函數體中)出現的 this.xxx=xxx 中的 this 是當前類的一個實例
  • call、apply 和 bind:this 是第一個參數
  • 箭頭函數 this 指向:箭頭函數沒有本身的 this,看其外層的是否有函數,若是有,外層函數的 this 就是內部箭頭函數的 this,若是沒有,則 this 是 window。

4、執行上下文棧(Execution Context Stack)

函數多了,就有多個函數執行上下文,每次調用函數建立一個新的執行上下文,那如何管理建立的那麼多執行上下文呢?

JavaScript 引擎建立了執行上下文棧來管理執行上下文。能夠把執行上下文棧認爲是一個存儲函數調用的棧結構,遵循先進後出的原則

從上面的流程圖,咱們須要記住幾個關鍵點:

  • JavaScript 執行在單線程上,全部的代碼都是排隊執行。
  • 一開始瀏覽器執行全局的代碼時,首先建立全局的執行上下文,壓入執行棧的頂部。
  • 每當進入一個函數的執行就會建立函數的執行上下文,而且把它壓入執行棧的頂部。當前函數執行完成後,當前函數的執行上下文出棧,並等待垃圾回收。
  • 瀏覽器的 JS 執行引擎老是訪問棧頂的執行上下文。
  • 全局上下文只有惟一的一個,它在瀏覽器關閉時出棧。

咱們再來看個例子:

var color = "blue";
function changeColor() {
    var anotherColor = "red";
    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }
    swapColors();
}
changeColor();

上述代碼運行按照以下步驟:

  • 當上述代碼在瀏覽器中加載時,JavaScript 引擎會建立一個全局執行上下文而且將它推入當前的執行棧
  • 調用 changeColor 函數時,此時 changeColor 函數內部代碼還未執行,js 執行引擎當即建立一個 changeColor 的執行上下文(簡稱 EC),而後把這執行上下文壓入到執行棧(簡稱 ECStack)中。
  • 執行 changeColor 函數過程當中,調用 swapColors 函數,一樣地,swapColors 函數執行以前也建立了一個 swapColors 的執行上下文,並壓入到執行棧中。
  • swapColors 函數執行完成,swapColors 函數的執行上下文出棧,而且被銷燬。
  • changeColor 函數執行完成,changeColor 函數的執行上下文出棧,而且被銷燬。

參考文章

關於Fundebug

Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等衆多品牌企業。歡迎你們免費試用

相關文章
相關標籤/搜索