概述

image.png
https://juejin.cn/post/7002763440800399391
js引擎执行过程分为三个阶段:

  1. 语法分析
  2. 预编译阶段
  3. 执行阶段

注:浏览器首先按顺序加载由<script>标签分割的js代码块,加载js代码块完毕后,立刻进入以上三个阶段,然后再按顺序查找下一个代码块,再继续执行以上三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的原理,并且都在同一个全局作用域中。

语法分析

js脚本代码块加载完毕后,会首先进入语法分析阶段。该阶段主要作用是:

分析该js脚本代码块的语法是否正确,如果出现不正确,则向外抛出一个语法错误(SyntaxError),停止该js代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入预编译阶段

预编译阶段

什么是执行上下文

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

执行上下文的类型

执行上下文总共有三种类型:

  • 全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  • 函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。
  • Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文

    执行栈

    执行栈。用于存储代码执行期间创建的上下文。其他语言叫做调用栈,具有LIFO结构。
    当v8引擎执行js代码的时候,首先会创建一个全局的执行上下文,并推入栈中,没当调用一个新的函数,会创建一个函数执行上下文并推入栈的最上面,执行完毕即出栈。
  1. let a = 'Hello World!';
  2. function first() {
  3. console.log('Inside first function');
  4. second();
  5. console.log('Again inside first function');
  6. }
  7. function second() {
  8. console.log('Inside second function');
  9. }
  10. first();
  11. console.log('Inside Global Execution Context');
  12. // Inside first function
  13. // Inside second function
  14. // Again inside first function
  15. // Inside Global Execution Context

image.png
chrome浏览器查看 call stack
image.png

创建执行上下文

执行上下文分两个阶段创建:1)创建阶段; 2)执行阶段

创建阶段

  • 1、确定 this 的值,也被称为 This Binding
  • 2、LexicalEnvironment(词法环境) 组件被创建。
  • 3、VariableEnvironment(变量环境) 组件被创建。

直接看伪代码可能更加直观

  1. ExecutionContext = {
  2. ThisBinding = <this value>, // 确定this
  3. LexicalEnvironment = { ... }, // 词法环境
  4. VariableEnvironment = { ... }, // 变量环境
  5. }

This Binding
  • 全局执行上下文中,this 的值指向全局对象,在浏览器中this 的值指向 window对象,而在nodejs中指向这个文件的global对象。
  • 函数执行上下文中,this 的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new绑定、箭头函数。

    词法环境(Lexical Environment)

    词法环境有两个组成部分

  • 1、环境记录:存储变量和函数声明的实际位置

  • 2、对外部环境的引用:可以访问其外部词法环境

词法环境有两种类型

  • 1、全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。
  • 2、函数环境:用户在函数中定义的变量被存储在环境记录中,包含了arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

直接看伪代码可能更加直观

  1. GlobalExectionContext = { // 全局执行上下文
  2. LexicalEnvironment: { // 词法环境
  3. EnvironmentRecord: { // 环境记录
  4. Type: "Object", // 全局环境
  5. // 标识符绑定在这里
  6. },
  7. outer: <null> // 对外部环境的引用
  8. }
  9. }
  10. FunctionExectionContext = { // 函数执行上下文
  11. LexicalEnvironment: { // 词法环境
  12. EnvironmentRecord: { // 环境记录
  13. Type: "Declarative", // 函数环境
  14. // 标识符绑定在这里 // 对外部环境的引用
  15. }
  16. outer: <Global or outer function environment reference>
  17. }
  18. }

变量环境

变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。
在 ES6 中,词法 环境和 变量 环境的区别在于前者用于存储函数声明和变量( letconst绑定,而后者仅用于存储变量( var绑定。
使用例子进行介绍

  1. let a = 20;
  2. const b = 30;
  3. var c;
  4. function multiply(e, f) {
  5. var g = 20;
  6. return e * f * g;
  7. }
  8. c = multiply(20, 30);

执行上下文如下:

  1. GlobalExectionContext = {
  2. ThisBinding: <Global Object>,
  3. LexicalEnviroment: {
  4. EnvironmentRecord: { // 环境记录
  5. Type: "Object", // 全局环境
  6. // 标识符绑定在这里
  7. a: < uninitialized >,
  8. b: < uninitialized >
  9. },
  10. outer: <null> // 对外部环境的引用
  11. },
  12. VariableEnviroment: {
  13. EnvironmentRecord: { // 环境记录
  14. Type: "Object", // 全局环境
  15. // 标识符绑定在这里
  16. c: undefined
  17. },
  18. outer: <null> // 对外部环境的引用
  19. }
  20. }
  21. MultiplyExectionContext = {
  22. ThisBinding: <Global Object>,
  23. LexicalEnviroment: {
  24. EnvironmentRecord: { // 环境记录
  25. Type: "Declarative", // 全局环境
  26. // 标识符绑定在这里
  27. Arguments: {0: 20, 1: 30, length: 2},
  28. },
  29. outer: <GlobalLexicalEnvironment> // 对外部环境的引用
  30. },
  31. VariableEnviroment: {
  32. EnvironmentRecord: { // 环境记录
  33. Type: "Object", // 全局环境
  34. // 标识符绑定在这里
  35. g: undefined
  36. },
  37. outer: <GlobalLexicalEnvironment> // 对外部环境的引用
  38. }
  39. }

tips:变量提升的原因:在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 letconst 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 letconst 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。

执行阶段

在此阶段,完成对所有变量的分配,最后执行代码。具体的js的执行阶段由于涉及到事件循环后面文章分析。
注: 在执行阶段,如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。