深入理解Javascript中的执行环境(Execution Context)和执行栈(Execution Stack)

如果你想成为一个Javascript开发者,那么你一定要知道Javascript程序的内部运行原理。理解执行环境和执行栈是非常重要的,其有助于理解其他Javascript的概念,比如说提升,作用域和闭包等。

当然,理解执行环境和执行栈的概念也将会使你成为一个更好的Javascript开发者。

闲话少说,马上开始吧。

## 执行环境是什么

简单来说,执行环境就是Javascript代码被计算和执行的环境的一个抽象概念。无论Javascript代码在什么时候运行,它都会运行在 执行环境中。

## 执行环境的类型

在Javascript中有三种执行环境的类型。

全局执行环境 - 这是一种默认和基础的执行环境。如果代码不在任何的函数中,那么它就是在全局执行环境中。他做了两件事情:首先,它创建了一个全局对象 - windows(如果是浏览器的话),并且把this的值设置到全局对象中。在程序中,只会存在一个全局执行环境。

函数执行环境 - 每次当函数被调用的时候,就会为该函数创建一个全新的执行环境。每个函数都有他们自己的执行环境,但是他们仅仅是在函数被调用的时候才会被创建。其可以有任意多个函数执行环境。无论新的执行环境在什么时候被创建,它都会按照定义的顺序依次执行一系列的步骤,不过这些我们稍后会讲。

eval函数执行环境 - 在eval函数中执行代码也会获得它自己的执行环境,但是eval并不经常被Javascript开发者所使用,所以这里我们目前并不打算讨论它。

## 执行栈

执行栈,在其他编程语言中也被称为调用栈,它是一种LIFO(后进先出)的结构,被用于在代码执行阶段存储所有创建过的执行环境。

当Javascript引擎首次运行到你的脚本时,它会创建一个全局执行环境,并把它推入到当前的执行栈中。每当引擎运行到其函数调用时,就会为这个函数创建一个新的执行环境,并把它推入到堆栈的顶部。

引擎会执行其执行环境位于堆栈顶部的函数。当函数执行完毕时,当前执行栈会从堆栈中弹出去,并且控件将会到达其在当前堆栈下面的那个执行环境中。

我们来通过下面的代码示例来理解:

    let a = 'Hello World!';

    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');



当上面的代码加载到浏览器中时,Javascript引擎会创建一个全局执行环境,并把它推到当前的执行栈中。当遇到对first()的调用时,Javascript引擎会为这个函数创建一个新的执行环境,并且把它推到当前执行栈的顶部。

当second()函数在first()函数内被调用时,Javascript引擎会为这个函数创建一个新的执行环境,并把它推送到当前执行栈的顶部。当second()函数完成的时候,它的执行环境会从当前的栈中推出去,并且空间会到达当前环境下面的那个执行环境中,也就是first()函数执行环境。

当first()完成以后,它的执行环境会会从堆栈中移出,并且控件会到达全局执行环境。当所有代码执行完以后,Javascript引擎会从当前栈中移出全局执行环境。

那么执行环境是如何被创建出来的呢?

到现在为止,我们已经看到Javascript引擎是如何管理执行环境的。那么现在咱们来理解一下执行环境是如何被Javascript引擎创建出来的吧。

执行环境的创建过程分为两个阶段:1,创建阶段,2,执行阶段。

## 创建阶段

执行环境是在创建阶段被创建出来的。在创建阶段会发生下面的事情:

词法环境组件被创建出来。

变量环境组件被创建出来。

因此执行环境从概念上可以被表示为:

    ExecutionContext = {

      LexicalEnvironment = <ref. to LexicalEnvironment in memory>,

      VariableEnvironment = <ref. to VariableEnvironment in  memory>,

    }

## 词法环境

官方ES6文档定义的词法环境如下:

词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义标识符与特定变量和函数的关联。词法环境由环境记录和一个对外部词汇环境的可能的空引用组成。

简单来说,词法环境是一个保存“变量-标识符”映射的结构。(标识符指向变量/函数的名称,变量是实际对象【包括函数对象和数组对象】的引用,或者是原始值)

例如,思考下面的代码片段:

    var a = 20;

    var b = 40;

    function foo() {

      console.log('bar');

    }

上面的代码片段的词法环境如下:

    lexicalEnvironment = {

      a: 20,

      b: 40,

      foo: <ref. to foo function>

    }

每一个词法环境都有三组件:

环境记录

对外层环境的引用

this绑定

## 环境记录

环境记录是变量和函数声明的地方,其被存储在词法环境内部。

有两种词法环境的类型:

声明环境记录 - 顾名思义,它存储变量和函数的声明。函数代码的词法环境包含一个声明环境记录。

对象环境记录 - 全局代码的词法环境包含一个对象环境记录。除了变量和函数声明之外,对象环境记录也会存储全局绑定对象(浏览器中的window对象)。因此对于每个绑定对象的属性(对于浏览器,它包含所有由浏览器给window对象的属性和方法),在记录中创建一个新的条目。

注意 - 对于函数代码,环境记录也会包含参数对象,参数对象包含传递给函数的参数以及索引,和传递给函数的参数的长度(个数)。例如,下面函数的参数对象看起来像这样子的:

    function foo(a, b) {

      var c = a + b;

    }

    foo(2, 3);

    // argument object

    Arguments: {0: 2, 1: 3, length: 2},

## 对外部环境的引用

对外部环境的引用意味着它可以访问外面的词法环境。这意味着如果他们在当前的词法环境中没有找到的话,Javascript引擎会在外面的环境里去寻找变量。

## this绑定

在这个组件中,this的值是确定的或者是已经设置的。

在全局执行环境中,this的值指向全局对象。(在浏览器中,this指向window对象)

在函数执行环境中,this的值依赖于函数的调用方式。如果它是在对象引用中被调用,this的值就被设置为那个对象,否则,this的值会被设置为全局对象或者是undefined(在严格模式中)。例如:

    const person = {

      name: 'peter',

      birthYear: 1994,

      calcAge: function() {

        console.log(2018 - this.birthYear);

      }

    }

    person.calcAge();

    // 'this' refers to 'person', because 'calcAge' was called with //'person' object reference

    const calculateAge = person.calcAge;

    calculateAge();

    // 'this' refers to the global window object, because no object reference was given

抽象的说,在伪代码中,词法环境看起来像这样:

    GlobalExectionContext = {

      LexicalEnvironment: {

        EnvironmentRecord: {

          Type: "Object",

          // Identifier bindings go here

        }

        outer: <null>,

        this: <global object>

      }

    }

    FunctionExectionContext = {

      LexicalEnvironment: {

        EnvironmentRecord: {

          Type: "Declarative",

          // Identifier bindings go here

        }

        outer: <Global or outer function environment reference>,

        this: <depends on how function is called>

      }

    }

## 变量环境:

它也是一个词法环境,其环境记录中环境记录保存着在运行环境中的VariableStatements创建的绑定。

正如上面所写的,变量环境也是一个词法环境,因此他有如上定义的词法环境的所有的属性和组件。

在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);

当上面的代码被执行的时候,Javascript引擎会创建一个全局的执行环境来执行这些全局代码。因此全局执行环境在创建阶段看起来像这样子的:

    GlobalExectionContext = {

      LexicalEnvironment: {

        EnvironmentRecord: {

          Type: "Object",

          // Identifier bindings go here

          a: < uninitialized >,

        b: < uninitialized >,

        multiply: < func >

      }

      outer: <null>,

        ThisBinding: <Global Object>

      },

      VariableEnvironment: {

        EnvironmentRecord: {

          Type: "Object",

          // Identifier bindings go here

          c: undefined,

        }

        outer: <null>,

        ThisBinding: <Global Object>

      }

    }

在运行阶段,变量赋值已经完成。因此全局执行环境在执行阶段看起来就像是这样的:

    GlobalExectionContext = {

      LexicalEnvironment: {

        EnvironmentRecord: {

          Type: "Object",

          // Identifier bindings go here

          a: 20,

          b: 30,

          multiply: < func >

        }

        outer: <null>,

        ThisBinding: <Global Object>

      },

      VariableEnvironment: {

        EnvironmentRecord: {

          Type: "Object",

          // Identifier bindings go here

          c: undefined,

        }

        outer: <null>,

        ThisBinding: <Global Object>

      }

    }

当遇到函数multiply(20,30)的调用时,一个新的函数执行环境被创建并执行函数中的代码。因此函数执行环境在创建阶段看起来像是这样子的:

    FunctionExectionContext = {

      LexicalEnvironment: {

        EnvironmentRecord: {

          Type: "Declarative",

          // Identifier bindings go here

          Arguments: {0: 20, 1: 30, length: 2},

        },

        outer: <GlobalLexicalEnvironment>,

        ThisBinding: <Global Object or undefined>,

      },

      VariableEnvironment: {

        EnvironmentRecord: {

          Type: "Declarative",

          // Identifier bindings go here

          g: undefined

        },

        outer: <GlobalLexicalEnvironment>,

        ThisBinding: <Global Object or undefined>

      }

    }

在这以后,执行环境会经历执行阶段,这意味着在函数内部赋值给变量的过程已经完成。因此此函数执行环境在执行阶段看起来就像这样的:

    FunctionExectionContext = {

      LexicalEnvironment: {

        EnvironmentRecord: {

          Type: "Declarative",

          // Identifier bindings go here

          Arguments: {0: 20, 1: 30, length: 2},

        },

        outer: <GlobalLexicalEnvironment>,

        ThisBinding: <Global Object or undefined>,

      },

      VariableEnvironment: {

        EnvironmentRecord: {

          Type: "Declarative",

          // Identifier bindings go here

          g: 20

        },

        outer: <GlobalLexicalEnvironment>,

        ThisBinding: <Global Object or undefined>

      }

    }

在函数执行完成以后,返回值会被存储在c里。因此全局词法环境被更新。在这之后,全局代码执行完成,程序运行终止。

注意:正如你所注意到的,let和const在创建阶段定义的变量没有值与他们相关联,但是var定义变量会设置为false。

这是因为,在创建阶段,扫描代码以查找变量和函数声明,当函数定义被全部存储到环境中时,变量首先会被初始化为undefined(在var的情况中),或者保持未初始化状态(在let和const的情况中)。

这就是你在他们定义之前(虽然是undefined)访问var定义的变量,但是当你在定义之前访问let和const定义的变量时,会得到一个引用错误。

这就是我们所谓的提升。

注意 - 在执行阶段,如果javascript引擎在源代码中声明的实际位置找不到let变量的值,那么它将为其分配未定义的值。

## 结论

所以我们已经讨论了如何在内部执行JavaScript程序。 虽然您没有必要将所有这些概念都学习成为一名出色的JavaScript开发人员,但对上述概念有一个正确的理解将有助于您更轻松,更深入地理解其他概念,如提升,作用域和闭包。

翻译自:

https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0

转载自:http://www.lht.ren/article/18/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容