Javascript 中的执行上下文

266 阅读6分钟

什么是执行上下文?

Javascript 代码执行时所在的环境被称为执行上下文。

执行上下文的类型

  1. 全局执行上下文:一个程序中只能有一个全局上下文。在全局上下文中会创建一个全局变量(在浏览器环境中就是 window),并且 this 指向全局变量。

  2. 函数执行上下文:函数执行上下文可以有任意多个。函数执行上下文是在函数被调用时创建的,每个函数有其自己的执行上下文。

  3. eval函数执行上下文:执行在eval函数内的代码也会有其自己的执行上下文(不经常使用)。

执行栈

用来保存代码运行时产生的执行上下文,是一种 LIFO(后进先出)的数据结构。

代码示例:

let a = 'hello world!'
function b () {
    console.log('b')
    c()
}
function c () {
    console.log('c')
}
b()

执行栈内的顺序是: 83A20F83-AD93-46AF-BC48-5CE18A4CF529.png

执行上下文

执行上下文中会分为2个阶段处理:创建阶段执行阶段

创建阶段

在Js代码执行前,执行上下文将经历创建阶段,会进行创建词法环境组件变量环境组件和进行this绑定

  • 词法环境组件

    在ES6文档中,词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。 简单来说就是词法环境标识符:变量的映射。标识符指变量或函数的名字,变量可以是js中的原始类型数据或引用类型数据。

    从ES6文档对词法环境的定义中可以知道,在词法环境中包含2个组成部分:

    • 环境记录器:保存变量(let和const声明)和函数声明
    • 外部环境的引用:可以访问父级的词法环境(作用域)

    词法环境包含两种类型:全局环境函数环境

    • 全局环境:在全局上下文中,词法环境就是全局词法环境。全局词法环境因为没有外部词法环境引用,所以外部词法环境引用是null。它拥有内置的属性和方法(Array、Object、Math等),还有用户定义的全局变量和函数声明,并且this会指向全局对象。

    • 函数环境:在函数中定义的变量保存在环境记录器中,外部词法环境的引用可以是全局词法环境,或者是包含此函数的外部函数的词法环境。

    环境记录器也包含两种类型:

    • 对象环境记录器:就是全局词法环境中的环境记录器,会保存全局上下文中定义的变量和函数声明。
    • 声明式环境记录器:就是函数词法环境中的环境记录器,会保存函数上下文中定义的变量、函数声明和传递给函数的arguments对象(索引-参数的映射,参数的length)。
  • 变量环境组件 变量环境组件就是一个词法环境组件,它有着词法环境组件的所有属性。在ES6中,词法环境和变量环境的区别是词法环境会保存函数声明和let、const声明的变量变量环境会保存var声明的变量

  • this绑定

    如果当前函数被作为对象方法调用或者使用 call、apply、bind等进行this绑定,那么this会被设置成那个对象,否则默认为全局对象调用

在执行上下文创建阶段,var定义的变量会初始化为undefined,let和const定义的变量不会被初始化,这就是所说的声明提升。在声明之前可以访问var定义的变量,但访问let和const定义的变量会报错。

执行阶段

进入执行上下文的执行阶段,完成对变量的分配,最后执行代码。

执行上下文流程

  1. 程序启动,全局执行上下文被创建

    1. 创建全局执行上下文词法环境

      • 创建对象环境记录器,处理在全局上下文中let和const定义的变量和函数声明

      • 创建外部环境引用,值为null

    2. 创建全局执行上下文变量环境

      • 创建对象环境记录器,处理全局上下文中var定义的变量,初始值为undefined 形成声明提升

      • 创建外部环境引用,值为null

    3. 确定this绑定值为全局对象(在浏览器中是window)

  2. 函数被调用,函数执行上下文被创建

    1. 创建函数执行上下文词法环境

      • 创建声明式环境记录器,处理函数中let和const定义的变量、函数声明和传递给函数的arguments对象(索引-参数的映射,参数的length)

      • 创建外部环境引用,值为全局对象,或者父级词法环境(作用域)

    2. 创建函数执行上下文变量环境

      • 创建声明式环境记录器,处理函数中var定义的变量,初始值为undefined 形成声明提升

      • 创建外部环境引用,值为全局对象,或者父级词法环境(作用域)

    3. 确定this绑定值

  3. 进入执行上下文执行阶段

    1. 在此阶段为变量分配对应的值,然后执行代码

通过伪代码辅助理解

let g1 = 1
const g2 = 2
var g3

function func(p1, p2) {
  let f1 = 1
  var f2 = 2
  return p1 + p2
}

g3 = func('hello ', 'world')

执行上下文结构

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      g1: < uninitialized >,
      g2: < uninitialized >,
      func: < function >
    }
    outer: <null>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      g3: undefined,
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      arguments: {0: 'hello ', 1: 'world', length: 2},
      f1: < uninitialized >
    },
    outer: <GlobalLexicalEnvironment>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      f2: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

相关总结

  • 程序开始运行时,会先生成全局执行上下文。函数被调用时,函数执行上下文才会被创建

  • 执行上下文通过执行栈来管理,它是一个LIFO(后进先出)的数据结构。全局上下文总是在执行栈的最底部,当函数被调用生成执行上下文,该上下文会被推入到执行栈顶部,在函数执行完毕后,该上下文会被弹出执行栈,执行流程到达执行栈中下一个上下文。

  • var定义的变量和函数声明存在声明提升。

  • 查找变量时,如果当前执行上下文中不存在,会去父级(函数声明时的父级词法环境)执行上下文中查找,一直查找到全局对象。由这些执行上下文组成的链表就是作用域链

相关参考