目录
语法/词法分析
编译原理中的词法分析和语法分析是编译过程中的两个基本阶段。它们的区别如下:
1. 词法分析:词法分析阶段负责将源代码按照字符序列划分成一个个的词法单元(token)。在词法分析过程中,词法分析器会读取源代码字符流并识别出其中的词法单元,如标识符、关键字、运算符、常量等。词法分析的输出是一个个的词法单元序列。词法分析器通常基于自动机理论实现,可以使用DFA(Deterministic Finite Automaton)或NFA(Non-deterministic Finite Automaton)进行词法分析。
2. 语法分析:语法分析阶段负责将词法单元序列转换为抽象语法树(Abstract Syntax Tree,AST)。在语法分析过程中,语法分析器会根据给定的语法规则,逐个分析识别出的词法单元,并构建词法单元之间的语法关系。语法分析的输出是一个抽象语法树,它形象地描述了源代码中的语法结构。语法分析器通常基于上下文无关文法(Context-Free Grammar)实现,可以使用LL(Left-to-right Leftmost derivation)、LR(Left-to-right Rightmost derivation)等算法进行语法分析。
综上所述,词法分析是将源代码划分为词法单元的过程,而语法分析是根据词法单元构建语法关系从而生成抽象语法树的过程。词法分析更关注源代码中的字符序列,而语法分析更关注词法单元之间的语法关系。词法分析和语法分析都是编译过程中非常重要的步骤,它们协同工作,构成了编译器的核心。
按语句块的粒度解析成抽象语法树 ,分析该js脚本代码块的语法是否正确,如果出现不正确,则向外抛出一个语法错误(SyntaxError),停止该js代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入预编译阶段, 此时不涉及到运行时;
预编译
预编译分为全局预编译和局部预编译,全局预编译发生在页面加载完成时执行,而局部预编译发生在函数执行的前一刻。 预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 。只有在解释执行阶段才会进行变量初始化 。
解释执行
js引擎解析代码,解析一行执行一行 ; 将实参带入形参, 直接运行编译好的代码块 ;
预编译什么时候发生
预编译分为全局预编译和局部预编译,全局预编译发生在页面加载完成时执行,而局部预编译发生在函数执行的前一刻。
tip:预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 。只有在解释执行阶段才会进行变量初始化 。
js运行三步曲
- 语法分析
- 预编译
- 解释执行
预编译前奏
imply global暗示全局变量,任何变量,如果变量未经声明就赋值,这些变量就为全局对象所有。一切声明的全局变量和未经声明的变量,全归window所有。
例如:
var a = 123;
window.a = 123;
下面这个函数里面只有一个连等的操作,赋值操作都是自右向左的,而b是未经声明的变量,所以它是归window的,我们可以直接使用window.b去使用它。
function test(){
// 这里的b是未经声明的变量,所以是归window所有的。
var a = b = 110;
}
预编译步骤
首先JavaScript的执行过程会先扫描一下整体语法语句,如果存在逻辑错误或者语法错误,那么直接报错,程序停止执行,没有错误的话,开始从上到下解释一行执行一行。
局部预编译的4个步骤:
- 创建AO对象(Activation Object)执行期上下文。
- 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
- 将实参值和形参统一。
- 在函数体里面找函数声明,值赋予函数体。
全局预编译的3个步骤:
- 创建GO对象(Global Object)全局对象。
- 找变量声明,将变量名作为GO属性名,值为undefined
- 查找函数声明,作为GO属性,值赋予函数体
由于全局中没有参数的的概念,所以省去了实参形参相统一这一步。
tip:GO对象是全局预编译,所以它优先于AO对象所创建和执行
巩固基础练习
关于AO对象的例子
// 函数
function fn(a){
console.log(a);
// 变量声明+变量赋值(只提升变量声明,不提升变量赋值)
var a = 123;
console.log(a);
// 函数声明
function a(){};
console.log(a);
// 函数表达式
var b = function(){};
console.log(b);
// 函数
function d(){};
}
//调用函数
fn(1);
1.预编译第1步:创建AO对象
AO{
}
2.预编译第2步:找形参和变量声明,将形参名和变量名作为AO对象的属性名
AO{
a : undefined,
b : undefined
}
3.预编译第3步:将实参值和形参统一
AO{
a : 1,
b : function(){...}
}
4.预编译第4步:在函数体里面找函数声明,值赋予函数体。
AO{
a : function a(){...},
b : undefined,
d : function d(){...}
}
最后输出结果:
// 函数
function fn(a){
console.log(a); //根据AO对象中的数据第一个打印的是:fn()
// 变量声明+变量赋值(只提升变量声明,不提升变量赋值)
var a = 123; // 执行到这时,由于变量赋值是不提升的,所以函数被123覆盖了
console.log(a); // 123
// 函数声明
function a(){}; // 这里被提升上去了,可以忽略
console.log(a); // 123
// 函数表达式
var b = function(){};
console.log(b); // 根据AO对象中的数据:fn()
// 函数
function d(){};
}
//调用函数
fn(1);
函数执行完毕,销毁AO对象。
关于GO对象的例子
global = 100;
function test(){
console.log(global);
var global = 200;
console.log(global);
var global = 300;
}
test();
var global;
1.全局预编译第1步:创建GO对象
GO{
}
2.全局预编译第2步:找变量声明,将变量名作为GO属性名,值为undefined
GO{
global:undefined
}
3.全局预编译第3步:查找函数声明,作为GO属性,值赋予函数体
GO{
global:undefined
}
4.局部预编译第1步:创建AO对象
AO{
}
5.局部预编译第2步:找形参和变量声明,将形参名和变量名作为AO对象的属性名
AO{
global: undefined
}
6.局部预编译第3步:将实参值和形参统一(此函数没有形参)
AO{
global: undefined
}
7.局部预编译第4步:在函数体里面找函数声明,值赋予函数体。(此函数内没有函数声明)
AO{
global: undefined
}
最的结果:
global = 100;
function test(){
console.log(global); // 根据AO对象中的数据:undefined
var global = 200; // 执行到这时,200覆盖了undefined
console.log(global); // 200
var global = 300;
}
test();
var global;
tip:关于GO对象和AO对象,它们俩是一个种链式关系,就拿上面的这个例子来说吧,如果在函数体的内部没有定义global变量,这也意味着AO对象中将有这个global这个属性。那如果没有会怎么办?它会去GO对象中寻找,说白了也就是一种就近原则。