前端笔记--JS执行上下文

580 阅读6分钟

写在前面:前段时间参加了金山的前端面试,面试过程中经常get不到面试官的点,有时候即使理解了面试官的意思,却又不能准确的表达出来。让我意识到当前最大的问题是表达沟通能力不足。所以期望通过撰写学习文档的方式,来提升自己的表达和总结的能力。

执行上下文

基本概念

执行上下文是指当前Javascript代码被解析和执行时所在环境的抽象概念,JavaScript 任何代码的运行都是在执行上下文中。

执行栈

js代码运行在执行栈中,执行栈也叫做调用栈,具有后进先出的栈结构,用于存储js代码在执行过程中创建的上下文环境。

<script>
    baz();
    foo();
    function baz() {
        console.log('in baz, we canot get bar: ', bar);
    }
</script>
<script>
    function foo() {
        console.log('in foo, wen can get bar: ', bar);
    }
    
    foo();
    var bar = 1;
    baz();
</script>

调用栈
以上面js文件执行为例,来简单阐述调用栈在函数执行过程中的状态。

  1. 在js引擎读取到页面js代码时,创建全局执行上下文,放到执行栈中。从第一段js文本中可以看出,此时全局对象中只包含baz这个函数变量。
  2. 创建完全局执行上下文之后,开始执行这段js代码,碰到baz(),将创建baz函数执行上下文。在函数执行过程中,我们尝试访问变量bar,但是在整个作用域链(下文解析作用域链概念)中都没有这个变量的定义。所以这时候会抛出referenceError错误。Uncaught ReferenceError: bar is not defined。然后终止函数执行,将baz执行上下文移除。
  3. 由于碰到异常,第一段js代码提前结束。
  4. 此时,继续解析第二段js代码,由于全局上下文已经被创建,则继续向这个上下文中添加环境变量。包括bar = undefined; foo: fun
  5. foo()执行foo函数,创建foo函数执行上下文,并执行,由于这时候bar并未被赋值,所以输出的bar值为undefined。执行完毕,移除foo上下文。
  6. 继续执行bar = 1赋值操作,然后执行函数baz。将创建baz执行上下文,因为只存在一个全局执行上下文,所以此时baz的外部环境对象的引用包括已经赋值的bar变量,所以会输出1。执行完毕,移除baz上下文。
  7. 当页面销毁时,移除全局执行上下文。

上下文分类

  1. 全局执行上下文 js引擎首次读取页面js文件时,会创建一个全局的执行上下文并推入执行栈。任何定义在全局中的变量都存在在这个上下文中。函数的运行也是基于这个全局执行上下文。
  2. 函数执行上下文 函数执行上下文只有在函数调用阶段才会被创建并放到调用栈中,函数执行完毕就会从调用栈中释放。在全局作用域无法访问函数作用域中定义的变量,就是因为函数中的变量只在函数调用生命周期中存在。(闭包情况例外,下文解释)
  3. eval函数执行上下文 运行在 eval 函数中的代码也获得了自己的执行上下文,Javascript不推荐使用eval函数。

如何工作

执行上下文分为两个阶段,创建阶段和执行阶段。

创建阶段

创建阶段主要完成三件事情。

  1. this绑定(这解释了为什么this的值取决于函数的调用方式)
  2. 创建词法环境。
  3. 创建变量环境

this绑定

根据当前函数的执行环境来绑定函数的this值,比如作为对象属性调用,this的值将被绑定到对象实例上。

词法环境

定义:词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。

词法环境的组成 词法环境分为两个部分:环境记录;对外部环境的引用。

  1. 环境记录是指存储变量和函数声明的实际位置
  2. 对外部环境引用意味着他可以访问其他词法作用域,闭包原理基于此。

词法环境分为两种:
1)全局环境:在全局执行上下文中,是一个没有外部环境的词法环境。其外部引用为null。它拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。
2)函数环境:用户在函数中定义的变量被存储在环境记录中,其外部引用可以是函数环境也可以是全局环境。对于函数环境而言,还包含一个arguments对象,该对象包含函数参数的引用,是一个类数组结构,如

function foo(a, b) {  
  var c = a + b;  
}  
foo(2, 3);

// arguments 对象  
Arguments: {0: 2, 1: 3, length: 2}

环境变量分为两种

  • 声明性环境记录 存储变量、函数和参数。一个函数环境包含声明性环境记录。
  • 对象环境记录 用于定义在全局执行上下文中出现的变量和函数的关联。全局环境包含对象环境记录。

变量环境

变量环境也是词法法环境,用于存储var声明的变量。

何谓声明提升

执行上下文在创建阶段,会扫描并解析变量和函数声明。其中函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 let 和 const 的情况下)。这就是所谓的声明提升

执行阶段

在此阶段,完成对所有变量的分配,最后执行代码。如果找不到let变量的值,将会为其分配undefined。

小结

理解函数执行上下文,可以让我们知道一段js代码按照什么样的顺序被执行和调用。有助于我们理解声明提升,闭包,函数作用域,this绑定等等概念。概括起来就是:

  1. js引擎在js首页被解析的时候,会创建一个全局执行上下文,这个全局上下文贯穿页面的整个生命周期。
  2. 函数被调用的时候,会创建函数的执行上下文,包含环境记录(收集函数声明和变量定义)和环境上下文引用(外部环境--闭包或者全局上下文)。函数执行完毕,函数上下文就会被移出调用栈。
  3. 执行上下文有两个阶段,创建阶段会绑定this,收集函数声明和变量定义以及外部环境引用。执行阶段会进行变量赋值与函数调用。

参考:【译】理解 Javascript 执行上下文和执行栈