深刻理解Javascript之Execution Context

1.概述

執行上下文(Execution Context)是執行 Javascript 代碼的環境。能夠絕不誇張地說,執行上下文是 Javascript 中最重要的概念。它是其餘不少重要概念的基礎。一旦搞清楚了執行上下文是什麼,咱們就能很輕鬆地掌握下面這些概念:javascript

  • 頂置(Hoisting)
  • 做用域鏈(Scope Chaining)
  • 閉包(Closure)
  • this以及arguments是如何賦值的
  • 等等等等

2.執行上下文

深刻理解 Javascript 之 CallStack&EventLoop一文中,咱們已經簡單瞭解了 Javascript 程序是如何執行以及函數調用的過程。咱們知道每次調用一個函數時,都會建立一個「調用信息」結構壓入調用棧。其實這個調用結構就是執行上下文。所以調用棧(Call Stack)也被稱爲執行棧(Execution Stack)。java

執行上下文有兩種類型:git

  • 一種爲全局執行上下文(Global Execution Context),程序開始時建立,有且只有一個。github

  • 另外一種爲局部執行上下文(Local Execution Context),調用函數時建立。局部執行上下文又稱爲函數執行上下文(Function Execution Context)。瀏覽器

看下面的代碼:bash

var name = "darjun";
var email = "leedarjun@gmail.com";

function greeting() {
    console.log(`Hi, I'm ${name}, my email is ${email}!`);
}

greeting();
複製代碼

Javascript 引擎在執行代碼時會建立一個全局對象(global object)。在瀏覽器中全局對象爲window對象,在 Node 環境中爲global對象。閉包

1) 在全部函數外層定義的變量都會保存在全局對象中。ide

2) 在函數內,未使用varletconst修飾的變量定義也會將變量存儲在全局對象中。函數

接下來引擎開始解析代碼,建立<main>函數包裹代碼。 而後,<main>函數執行。此時,Javascript 引擎首先會建立一個全局執行上下文。工具

執行上下文的建立分爲兩個階段:

1)建立階段(Creation Phase)

2)執行階段(Execution Phase)

在全局執行上下文的建立階段,引擎將進行以下處理:

1)綁定this到全局對象。

2)建立一個全局環境對象(Global Environment)。爲<main>中定義的變量和函數分配內存。var定義的變量初始值爲undefined

此時,全局執行上下文以下所示:

GlobalExecutionContext = {
    Phase: Creation, // 建立階段
    this: GlobalObject,
    GlobalEnvironment: {
        name: undefined,
        email: undefined,
        greeting: fn,
    }
}
複製代碼

注意:此時代碼還未執行。

接下來,引擎開始從上到下,一行一行地執行<main>函數。

首先,引擎將全局執行上下文壓入調用棧。這時全局執行上下文切換爲執行階段(Phase: Creation -> Execution)。而後,跳過函數定義。由於greeting函數在建立階段就已經被解析完成而且放入全局環境對象中了。而後執行到代碼greeting();調用greeting函數。

引擎首先爲函數greeting建立一個局部執行上下文。局部執行上下文的建立也將經歷建立和執行兩個階段。建立階段時,引擎執行以下處理:

1)根據調用方式綁定this變量。在這個例子中,函數greeting是全局函數,沒有對象限定。this被綁定到全局對象。

2)建立一個局部環境對象(Local Environment)。該對象與全局環境對象做用相似,只不過是爲函數中定義的變量和函數分配內存。該對象中有一個指向外層環境對象的指針outer

這時的局部執行上下文以下所示:

Greeting ExecutionContext = {
    Phase: Creation, // 建立階段
    this: GlobalObject,
    LocalEnvironment: {
        // 沒有變量或函數定義
        outer: <GlobalEnvironment>
    },
}
複製代碼

引擎將該局部執行上下文壓入調用棧開始執行。greeting執行完成以後,從調用棧上彈出其局部執行上下文。此時棧頂只有一個全局執行上下文,繼續執行<main>

<main>執行完成,將全局執行上下文從調用棧中彈出,程序結束。

4.應用執行上下文理解其餘概念

上面咱們瞭解了什麼是執行上下文,而且深刻到程序執行內部觀察到引擎是怎麼處理函數調用的。接下來,咱們將運用執行上下文來了解 Javascript 的幾個核心概念。

頂置

頂置實際上是因爲 Javascript 特殊的執行邏輯而出現的。咱們先修改一下前面的示例代碼:

console.log(name);
console.log(email);

var name = "darjun";
var email = "leedarjun@gmail.com";

function greeting() {
    console.log(`Hi, I'm ${name}, my email is ${email}!`);
}

greeting();
複製代碼

代碼前兩行的輸出是什麼?

咱們知道一個執行上下文會經歷建立和執行兩個階段。在建立階段時,引擎首先爲函數中定義的變量和函數分配內存空間並存入環境對象中。var定義的變量初始化爲undefined,函數直接解析完成。 而後,引擎壓入該執行上下文,一行一行執行代碼。

那麼很清楚了,前兩行的輸出都是undefined。由於在執行上下文的建立階段,nameemail會被初始化爲undefined。這就形成變量或函數還未定義就能直接使用的假象,看起來好像var變量和函數定義被「提高」或「頂置」到代碼的最前面同樣。一樣的道理,在代碼最上面也能夠打印函數greeting,將打印出具體的函數對象。由於頂層函數在建立階段就已經存在環境對象中了。快試試🤩。

var的這種特性常常會形成意想不到的結果,因此 ES6 引入了另外一種變量定義方式letlet定義的變量在定義以前引用會拋出異常。這是怎麼作到的呢?

其實很簡單。在執行上下文的建立階段,let定義的變量也會存入環境對象中。不過,它的初始值爲UnInitialized(未初始化)。在執行時,若是引用一個值爲UnInitialized的變量,引擎直接拋出一個錯誤🥴。

閉包

是指函數中能訪問在函數外層定義的變量,這個函數加上外層的環境就構成了一個閉包。咱們仍是經過案例來分析:

function makeAdder(num) {
    return function (x) {
        return x + num;
    }
}

var adder2 = makeAdder(2);
console.log(adder2(10)); // 12

var adder5 = makeAdder(5);
console.log(adder5(10)); // 15
複製代碼

第一次調用函數makeAdder時,傳入參數2,返回一個匿名函數賦值給變量adder2。這時,makeAdder函數已返回。可是adder2調用時能正確返回12。說明adder2能訪問到以前傳入的參數num

第二次調用函數makeAdder時,傳入參數5,返回一個匿名函數賦值給變量adder5。此時,makeAdder函數已返回。可是adder5調用時能正確返回15。說明adder5能訪問到以前傳入的參數num而且,adder2adder5訪問到的num變量相互獨立(一個爲2,一個爲5)

運用執行上下文模擬一次程序執行過程,能很清楚的看到閉包的工做原理。

參數num至關因而在函數內定義的變量。 首先,第一次調用makeAdder時。引擎爲這次調用建立一個新的局部環境對象,num被保存在此對象中:

makeAdder LocalEnvironment2 = {
    num: 2,
}
複製代碼

adder2被調用時,引擎會建立一個新的局部環境對象。該對象中保存着x = 10,而且其outer指針指向上面的LocalEnvironment2

adder2 LocalEnvironment = {
    x: 10,
    outer: <makeAdder LocalEnvironment2>
}
複製代碼

adder2執行過程當中,訪問變量num。引擎首先在adder2的局部環境對象中查找num,沒有找到。而後引擎會到其外層的環境對象中繼續查找,直到找到該變量。或者直到全局環境對象中也未能找到,拋出引用錯誤。 在該示例中,外層環境對象中查找到num2adder2(10)執行完成,輸出12

第二次調用makeAdder時。引擎爲這次調用建立一個新的局部環境對象,num被保存在此對象中:

makeAdder LocalEnvironment5 = {
    num: 5,
}
複製代碼

adder5被調用時,引擎會建立一個新的局部環境對象。該對象中保存x = 10,而且其outer指針指向上面的LocalEnvironment5

adder5 LocalEnvironment = {
    x: 10,
    outer: <makeAdder LocalEnvironment5>
}
複製代碼

執行代碼return x + num時,按照上面的變量查找流程,在外層環境對象LocalEnvironment5中找到的num值爲5adder5(10)執行完成,輸出15

arguments

咱們知道,在函數調用中,arguments對象中包含傳入的全部參數、參數的長度以及其餘一些信息。例如:

function f(a, b, c) {
    console.log(arguments);
}

f(1, 2); // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
複製代碼

參數列表在調用時會依次被賦予傳入的實參。調用時的局部對象會包含全部參數變量,arguments等:

LocalEnvironment = {
    a: 1,
    b: 2,
    arguments: [1, 2] // ...
}
複製代碼

this綁定

先看一段代碼:

var person = {
    name: "darjun",
    age: 29,
    greeting: function () {
        console.log(`Hi, I'm ${this.name}, ${this.age} years old`);
    }
}

person.greeting(); // 輸出 Hi, I'm darjun, 29 years old

var g = person.greeting;

g(); // 輸出 Hi, I'm undefined, undefined years old
複製代碼

前面咱們知道 Javascript 引擎在執行一個函數前會進行this綁定。具體爲this綁定什麼值,視調用形式而定。

在上面的代碼中,第一次調用greeting函數時,經過對象person限定,引擎會將person綁定爲this。 第二次調用前,將person.greeting賦值給變量g。而後直接調用函數g,引擎看到這次調用沒有.限定符,故而將this綁定爲全局對象。 因此輸出爲"Hi, I'm undefined, undefined years old"(注意:輸出視全局對象中是否有nameage屬性而有所不一樣)。

4.可視化

這裏我給你們推薦一個可視化查看程序執行的工具:javascript-visualizer

頂置:

閉包:

工具並不完善,可是很是有助於咱們理解執行上下文。很是值得一試🤩。

5.總結

我認爲執行上下文是 Javascript 中最最重要的概念。掌握了執行上下文,咱們能很深入地洞悉 Javascript 程序的運行機理,能很輕鬆地理解其餘的一些重要概念:頂置(Hoisting)、閉包(Closure)、thisarguments等。

掌握執行上下文,真的能稱霸 Javascript 世界哦🤩。

6.參考連接

  1. Javascript: What Is The Execution Context? What Is The Call Stack
  2. Understand Execution Context and Execution Stack in Javascript
  3. The Ultimate Guide to Hoisting, Scopes, and Closures in JavaScript

關於我: 我的主頁 簡書 掘金

相關文章
相關標籤/搜索