简述

对于常见编译型语言(例如:Java)来说,编译步骤分为:词法分析->语法分析->语义检查->代码优化和字节生成。
对于解释型语言(例如JavaScript)来说,通过词法分析和语法分析得到语法树后,就可以开始解释执行了。

编译阶段

词法分析

词法分析是将字符流(char stream)转换为记号流(token stream),就像英文句子一个个单词独立翻译,举例:
代码:var result = testNum1 - testNum2;
词法分析后 :

NAME "result"  
EQUALS  
NAME "testNum1"  
MINUS  
NAME "testNum2"  
SEMICOLON 

语法分析

语法分析得到抽象语法树

{
  "type": "Program",
  "start": 0,
  "end": 34,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 34,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 34,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 10,
            "name": "result"
          },
          "init": {
            "type": "BinaryExpression",
            "start": 15,
            "end": 34,
            "left": {
              "type": "Identifier",
              "start": 15,
              "end": 23,
              "name": "testNum1"
            },
            "operator": "-",
            "right": {
              "type": "Identifier",
              "start": 26,
              "end": 34,
              "name": "testNum2"
            }
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}

当JavaScript解释器在构造语法树的时候,如果发现无法构造,就会报语法错误,并结束整个代码块的解析。

预编译

当JavaScript引擎解析脚本时,它会在预编译期对所有声明的变量和函数进行处理!并且是先预声明变量,再预定义函数。如果存在变量提升,则先提升变量再提升函数,但是字面量定义的函数不会被提升,即是变量提升会被函数提升覆盖,但是同名的函数与变量会被后面的变量赋值所覆盖

执行阶段

JavaScript有两个重要的组成部分,一个是内存堆,一个是执行栈

内存堆--内存分配的地方

如下代码,变量name和函数hello就是被存放在内存堆中

var name = 'David'
function hello(name){
    console.log(`hello,${name}!`)
}

执行栈--代码执行的地方

当我们调用这个函数时,即hello(),这个时候它就会被压进执行栈中

执行上下文

执行上下文在代码块执行前创建,主要有三种

  • 全局执行上下文 — 这是基础上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。
  • Eval 执行上下文 — 执行在 eval 内部的代码也会有它属于自己的执行上下文,不推荐使用。

执行上下文分为两个阶段

  • 创建阶段
  • 执行阶段

创建阶段

执行上下文的创建阶段主要解决以下三点:

  • 决定 this 的指向
  • 创建词法环境(LexicalEnvironment)
  • 创建变量环境(VariableEnvironment)

this指向

我们应该知道this的指向是在代码执行阶段确定的,所谓的『代码执行阶段』正是『执行上下文的创建阶段』。默认情况下this指向全局对象,比如浏览器中的window.此外可能存在隐式绑定的情况。

词法环境

词法环境分为三大类:

  • 全局环境:全局环境的外部环境引用是 null,它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。
  • 模块环境:包含模块顶级声明的绑定以及模块显式导入的绑定。 模块环境的外部环境是全局环境。
  • 函数环境:函数内部用户定义的变量存储在环境记录器中,外部引用既可以是其它函数的内部词法环境,也可以是全局词法环境

词法环境本身包括两个部分:

  • 『环境记录器(Environment Record)』是存储变量和函数声明的实际位置
  • 『外部环境的引用(outer Lexical Environment)』指它可以访问其父级词法环境(即作用域)

变量环境

变量环境也是一个词法环境,但不同的是词法环境被用来存储函数声明和变量(let 和 const)绑定,而变量环境只用来存储 var 变量绑定。

阐述几个问题

this是怎么被绑定的?

在创建可执行上下文的时候,根据代码的执行条件,来判断分别进行默认绑定、隐式绑定、显示绑定等。

作用域链是怎么形成的?

可执行上下文中的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,这些引用串联起来一直指向全局的词法环境,因此形成了作用域链。

闭包是怎么形成的?

可执行上下文中的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,因此形成了闭包。