一 词法分析
词法分析到底是何物?
相信大家在工作生活中或多或少都听说过,但是却少有人真正懂得词法分析到底有何功用,怎么用,这里面的到底有多少奥秘,值得我们每一个从事前端开发者都需要好好研究下。
首先我们来看看常见编程语言是如何运行的?
编程语言分为编译型语言和解释性语言。
编译型语言(如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为:
上述的过程可以理解为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浏览器的代码运行过程:
- bar 函数被调用
- 执行bar函数的代码及当前的作用域
- 执行foo函数及当前的作用域
有图有真相,证明javascript确实是基于静态作用域的。
四 动态作用域
动态作用域 是在运行时根据程序的流程来动态确定的。
动态作用域并不在乎函数和作用域是如何声明的或者在何处声明的,它只关心从何处调用。
作用域是基于调用栈的,而不是代码中的作用域嵌套。
还是上个例子,假如 javascript 是基于动态作用域的,因为foo函数无法找到a变量时,会顺着调用栈在调用foo的地方找a,而不是在嵌套的词法作用域链中向上查找。由于bar是在foo中被调用的,故会在bar的作用域,并在其中找到a,会输出3。
但是很显然,javascript并没有这么做
结果再次证明了javascript是基于静态作用域的。