JS执行上下文


JS执行上下文

JS代码执行流程

代码被 JS引擎放入内存,先编译阶段,再执行阶段
特点:先编译后执行
编译阶段 会生成两部分内容,执行上下文(Execution context) 和 可执行代码
编译阶段 变量 函数 放在执行上下文(变量环境)中,变量默认值被设为undefined

执行阶段 按顺序一行一行执行 可执行代码
执行阶段 执行过程, JS引擎从 变量环境 中查找 变量和函数

实际上 两个阶段非常复杂,包括词法分析,语法解析,代码优化,代码生成等

从调用栈角度

JS使用 栈结构 管理 执行上下文,这个栈称为 执行上下文栈(调用栈)

  1. 编译JS文件,得到 全局执行上下文 和 全局可执行代码,将 全局执行上下文 压入栈底,执行 全局可执行代码,
  2. 碰到函数调用,将 函数编译,得到 函数执行上下文 和 函数可执行代码 ,将 函数执行上下文 压入栈中, 执行函数可执行代码
  3. 如果 碰到函数执行重复上一步 否则继续执行,
  4. 直到 函数return,函数执行上下文栈 弹出,继续回到上一次执行可执行代码
  5. 最后全局可执行代码执行完毕,弹出 全局执行上下文

栈溢出:调用栈有大小限制,入栈执行上下文超过一定数目,JS引擎会报错
一般栈溢出 是 没有终止 循环调用 或 递归调用 导致的

宏观角度(抽象角度)

执行上下文

执行上下文:当前JS代码 解析和执行 时所在环境的抽象概念

分为三种:
全局执行上下文,1.创建全局对象(window对象) 2.this指向这个全局对象

函数执行上下文,1.函数被调用时创建 2.每个函数都有自己的函数执行上下文

Eval函数执行上下文,1.eval函数内代码的执行上下文

执行栈

执行栈(调用栈),LIFO(后进先出),储存 代码执行期间创建的所有执行上下文

JS引擎 首次读取脚本,创建 全局执行上下文,推入 执行栈,

执行到函数调用, 会为该函数创建 函数执行上下文,推入 执行栈顶端.

JS引擎 会执行 其执行上下文位于执行栈顶端的函数,完成后 当前执行上下文弹出,

上下文控制权 移到 下一个执行上下文

执行上下文的创建

执行上下文分为两个阶段创建: 1. 创建阶段 2. 执行阶段

创建阶段:

  1. 绑定this(This Binding)
  2. 创建 词法环境(LexicalEnvironment)
  3. 创建 变量环境(VariableEnvironment)
// 执行上下文伪代码
ExecutionContext = {  
  ThisBinding = <this value>,  
  LexicalEnvironment = { ... },  
  VariableEnvironment = { ... },  
}

词法环境 和 变量环境

词法环境(Lexical Environment),
一个 标识符(变量/函数的名称) 与 变量(实际对象/原始值的引用) 相映射的结构,
包括
1.环境记录(environment record) 储存变量和函数生命的实际位置
2. 对外部环境的引用 意味着可以访问外部词法环境,

词法环境 存在两种类型
1.全局环境(在全局上下文中)对外部的环境引用为null
2.函数环境(在函数环境中)包含了一个 arguments对象

环境记录,存在两种类型
2.对象环境记录(在全局环境中)用于定义 全局执行上下文出现的变量和函数的关联
1.声明性环境记录(在函数环境中) 储存 变量/函数/参数

arguments对象,伪数组,包含了 函数参数 与 索引 的映射,及 参数个数.

// 词法环境伪代码
GlobalExectionContext = { // 全局执行上下文
  LexicalEnvironment: { // 词法环境(全局环境)
    EnvironmentRecord: {  // 环境记录(对象)
      Type: "Object",  
      // 标识符绑定在这里 
    outer: <null>  
  }  
}

FunctionExectionContext = { // 函数执行上下文
  LexicalEnvironment: { // 词法环境(函数环境)
    EnvironmentRecord: {  // 环境记录(声明性)
      Type: "Declarative",  
      // 标识符绑定在这里 
    outer: <Global or outer function environment reference>  
  }  
}

变量环境(VariableEnvironment)

1.变量环境 就是 词法环境,具有 词法环境 的所有属性
2.在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);

执行上下文如下

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

执行上下文的第二个创建阶段,执行阶段,此几段完成所有变量的分配,再执行代码.

执行阶段 找不到 给 let变量的赋值,则会为其赋值 undefined

词法环境栈 每个词法环境,内部都维护了一个小型栈结构,栈底是最外层的执行上下文的变量,进入一个作用域块(如if)后,该作用域块内的变量压到栈顶,作用域执行完后,作用域从栈顶弹出,这就词法环境的结构.

总结
块级作用域 通过 词法环境 和栈结构来实现
变量提升 通过 变量环境 来实现

当前 执行上下文 内的变量查找顺序,
沿着当前 执行上下文 内的词法环境栈 顶向下查找,找到栈底,再找 变量环境,
如果 没找到,则通过 作用域 链,查找外层 执行上下文

作用域链 和 this链

作用域链

作用域链 查找变量时,JS引擎会尝试在当前作用域下查找,如果没有找到,会再到外层作用域查找,以此类推直到找到,或到了全局作用域还没找到,报错。这样查找的 执行上下文链条 叫作用域链。

而JS在执行过程中,其作用域链(外层执行上下文是谁)由词法作用域决定
词法作用域(静态作用域) 作用域链在 词法化阶段(编译阶段)决定,也就是由 函数声明的位置 决定,与 函数调用位置无关

不要混淆了词法环境和词法作用域,
词法作用域是在代码编译阶段确定的作用域(一个抽象的概念),
词法环境是Javascript引擎用来存储变量和对象引用的地方(一个具象的概念)。
变量环境/词法环境 中的 外部引用(outer) 就说用来指向外层的 执行上下文,产生作用域链

变量查找顺序:
1.沿着当前 执行上下文内 的 词法环境栈 顶向下查找,找到栈底,再找 变量环境,
2.沿着 作用域链,在外部作用域中查找 外一层 的执行上下文,重复第一步。

理解闭包的产生比理解闭包的概念更重要,
闭包的产生,根据词法作用域规则,函数的作用域链由函数声明位置决定,声明位置在内部的函数总是可以访问外部函数的变量,即使内部函数的调用位置,不在其声明的外层函数内,甚至外部函数已经执行结束,但内部函数 引用的外部函数变量依旧保存在内存中,这些变量的集合称为闭包.
闭包,函数与其外部函数词法环境的集合.(函数,自己内部的变量,外部词法环境,可访问的外部变量)

this链

首先,作用域链this 是两套 完全不同 的系统,
作用域链 用于 函数语句执行查找变量值,由函数声明位置决定,受函数嵌套影响
thisthis链 用于 对象内部查找对象属性值(函数也是对象),受调用链影响,默认指向window,不受函数嵌套影响

全局执行上下文中的this,指向window对象,作用域链的最底端也包含window对象
函数执行上下文的this,默认调用时指向window对象,以下三种情况将改变this指向,
1.call,apply,bind,设置函数执行上下文的this
2.对象.函数(),通过对象调用,this指向对象本身
3.构造函数的this,指向new关键词构建的新对象.

全局调用函数相当于window.函数()

缺陷,嵌套函数的this,不继承外层函数的this,反直觉.
解决方案,1.self保存this,在内层函数中调用self,把this体系转换为作用域体系,
解决方案,2.ES6箭头函数,箭头函数 不会创建自身指向上下文,所以this取决于外部函数

严格模式,函数被正常调用,this值为undefined

执行上下文:变量环境,词法环境,outer,this

微观角度(代码角度)

词法环境 变量环境 活动对象AO 变量对象VO

参考:【译】理解 Javascript 执行上下文和执行栈
Damonare理解Javascript的作用域和作用域链


文章作者: 罗紫宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗紫宇 !
  目录