你不知道的JavaScript:深入浅出对编译原理和作用域的理解

196 阅读5分钟

编译原理

词法分析:

词法分析(LexicalAnalysisLexical Analysis),又称扫描(Scanning),是编译过程中的第一个阶段。此阶段的任务是将输入的源代码字符串转换为一系列标记(Token)或词法单元(Lexical Unit)。这些标记是源代码的基本构成元素,如关键字、标识符、运算符、分隔符和常量等。例如var a = 2就会把它分析出来为"var","a","=","2"这四部分。

语法分析:

它根据词法分析得到的词法单元(Token)序列,然后对这些词法单元流(Token Stream)进行解析,然后将这些词法单元转换成抽象语法树(Abstract Syntax Tree, AST)语法树是程序语法结构的直观表示,后续的代码生成和优化等阶段都会基于语法树进行。

生成代码:

编译器或解释器会根据前面阶段(如语法分析和语义分析)构建的语法树(如抽象语法树,AST)和符号表等信息,生成目标机器或虚拟机能够执行的代码(像字节码,机械码等)。例如上面var a = 2就会被转换为一个机械指令,创建一个变量a,将值2存于变量a当中。

作用域

js有全局作用域和函数作用域,作用域法则;是内层作用域可以访问外层作用域外层作用域不能访问内层作用域

词法作用域

词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,可以看下面的例子

function foo(a) {
       var b = a * 2;
       function bar(c) {
               console.log( a, b, c );
       }
       bar( b * 3 );
}
foo( 2 ); //输出结果为 2, 4, 12

1,包含着整个全局作用域,其中只有一个标识符:foo

2,包含着 foo 函数方法所创建的作用域,其中有三个标识符:a、bar 和 b。

3,包含着 bar 函数方法所创建的作用域,其中只有一个标识符:c

欺骗词法作用域

如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修 改”(也可以说欺骗)词法作用域呢?JavaScript 中有两种机制来实现这个目的

1,eval

function foo(str,a){
    eval(str)
    console.log(a,b);
}
foo('var b = 3',1)

调用中的 "var b = 3" 这段代码会被当作本来就在那里一样来处理。由于那段代码声明了一个新的变量 b,因此它对已经存在的 foo(..) 的词法作用域进行了修改,就相当于下面的代码。

function foo(str,a){
    var b=3
    console.log(a,b);
}
foo('var b = 3',1)

事实上,和前面提到的原理一样,这段代码实际上在 foo(..) 内部创建了一个变量 b,并遮蔽 了外部(全局)作用域中的同名变量,

2,with

with一般用于批量修改一个对象中的属性值,例如下面代码

var obj = {
    a: 1,
    b: 2,
    c: 3
}
with (obj) {
    a = 2
    b = 3
    c = 4

}
console.log(obj) 

with修改后的输出结果为{ a: 2, b: 3, c: 4 }

在这里我们要讲它如何作为欺骗语法,如果我们要修改的对象中不存在的属性值,那么该属性值就会泄露到全局作用域上面,例如下面代码

var obj = {
    a: 1
}
with(obj){
    b=2
}
console.log(obj,b);

这里with修改属性b,但在它修改的对象中不存在b这个属性,那么修改后的属性值就会泄露为全局变量,下面是代码的输出结果

微信截图_20241112144740.png

块级作用域

for (var i = 0; i < 10; i++) {
    //console.log(i);
}
console.log(i); 

for循环是一个语句,在JavaScript中只有全局作用域和函数体作用域,显然var i = 0是全局中的标识符,所有循环体外面也能访问到i,这里的输出结果就是

1.png 但是我们把var关键字换为let,就会形成块级作用域

for (let i = 0; i < 10; i++) {
    //console.log(i);
}
console.log(i); 

此时全局下面就访问不到i了

2.png 所以块级作用域是let + {}形成的

那是整个{}里面的标识符都会变为块级作用域吗,看看下面代码

  if (true) {
        let a = 1
        var b = 2
    }
    console.log(b);

{}外面a肯定是访问不到了,那么b呢,下面是代码的输出结果

3.png 由此可见b是可以正常拿到的,所以let+{}并不是将整个{}里面的内容都变为块级作用域,而是将let关键字声明的变量转变为块级作用域

结语

本文概述了编译原理中的词法分析、语法分析和代码生成阶段,以及JavaScript中的作用域机制,特别是词法作用域、块级作用域以及欺骗词法作用域的两种方式(evalwith)。

词法分析将源代码转换为标记序列,语法分析将这些标记转换为抽象语法树。代码生成阶段则基于语法树等信息生成可执行代码。

JavaScript具有全局作用域和函数作用域,词法作用域由代码书写位置决定。let{}形成块级作用域,限制变量可见性。evalwith机制可“欺骗”词法作用域,但使用需谨慎,以避免潜在风险