彻底搞懂词法分析、语法分析、静态作用域、动态作用域

Posted by franki on September 20, 2019

一 词法分析

词法分析到底是何物?

相信大家在工作生活中或多或少都听说过,但是却少有人真正懂得词法分析到底有何功用,怎么用,这里面的到底有多少奥秘,值得我们每一个从事前端开发者都需要好好研究下。

首先我们来看看常见编程语言是如何运行的?

编程语言分为编译型语言和解释性语言。

编译型语言(如java)的编译过程是:

词法分析 > 语法分析 > 语意检查 > 代码优化和字节码生成

解释性语言(如javascript)的编译过程是:

词法分析 > 语法分析 > 语法树

这里只针对javascript解析器的工作流程进行分析;

词法分析: javascript解析器将javascript字符串(字符流)按照ECMAScript标准转换为记号流。

字符流:

var ast = 'is tree';

记号流:

NAME "ast"
EQUALS
NAME "is tree"
SEMICOLON

二 语法分析

将词法分析转为AST(abstract struct tree),中文为抽象语法树

举个例子

var a = 1;

记号流Tokens为:

[
    {
        "type": "Keyword",
        "value": "var"
    },
    {
        "type": "Identifier",
        "value": "a"
    },
    {
        "type": "Punctuator",
        "value": "="
    },
    {
        "type": "Numeric",
        "value": "1"
    },
    {
        "type": "Punctuator",
        "value": ";"
    }
]

Synatax:

{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "a"
                    },
                    "init": {
                        "type": "Literal",
                        "value": 1,
                        "raw": "1"
                    }
                }
            ],
            "kind": "var"
        }
    ],
    "sourceType": "script"
}

完整的ast为:

ast

上述的过程可以理解为javascript语法分析器在经过词法分析生成的记号流,把记号流按照ECMAScript标准按照词法分析所产生的记号生成语法树。

简单来说,就是把程序收集到的信息存入数据结构,每次取记号就进行语法分析。

语法检查过后的后续过程为:

  • 预解析
  • 执行上下文
  • 执行代码

三 静态作用域

先普及下作用域的概念

作用域就是当前执行代码对于标识符的访问权限

通俗来说,编译器会在当前作用域声明一些变量,运行时引擎会去找相应的这些变量,如果找到了就可以去操作这些变量,找不到就继续往上找(形成一条链,业界把它叫做作用域链),还是找不到则返回null。

静态作用域 也被叫做 词法作用域 ,它的作用域在词法分析阶段就已经产生,不会再改变。

来看看这段代码,看看输出的是什么?

var a = 2;
function foo() {
    // 输出2还是3
    console.log(a);
}

function bar() {
    var a = 3;
    foo();
}

bar();

首先下个结论:javascript 是基于静态作用域的,也就是作用域在写代码的时候已经决定了,而不是等到代码执行的时候才决定。

首先foo函数回去全局找到全局作用域的a变量,因此输出2

看看在chrome浏览器的代码运行过程:

  1. bar 函数被调用

scope-1

  1. 执行bar函数的代码及当前的作用域

scope-2

  1. 执行foo函数及当前的作用域

scope-3

有图有真相,证明javascript确实是基于静态作用域的。

四 动态作用域

动态作用域 是在运行时根据程序的流程来动态确定的。

动态作用域并不在乎函数和作用域是如何声明的或者在何处声明的,它只关心从何处调用。

作用域是基于调用栈的,而不是代码中的作用域嵌套。

还是上个例子,假如 javascript 是基于动态作用域的,因为foo函数无法找到a变量时,会顺着调用栈在调用foo的地方找a,而不是在嵌套的词法作用域链中向上查找。由于bar是在foo中被调用的,故会在bar的作用域,并在其中找到a,会输出3。

但是很显然,javascript并没有这么做

结果再次证明了javascript是基于静态作用域的。