一看就懂的JavaScript预处理编译和作用域

448 阅读6分钟

一、引言:

提到Javascript的编译,我们不得不深入了解浏览器的内置引擎了,最为大家所熟知的几款流行浏览器之一的 —— Google Chrome ,它内置的V8 JavaScript引擎专为高性能设计,通过高效的即时编译(JIT)技术和优化编译策略,能够快速将JavaScript代码转换为机器码执行。

二、v8引擎解读代码的编译原理:

几乎所有浏览器都会内置JavaScript引擎,由于有Google开发的高性能引擎,我们就以它为学习例子,这也方便以后的代码调试。(建议大家都使用Google Chrome)

JavaScript编译原理:

  • 词法分析: 这个过程就是将由字符组成的字符串分解成引擎能读懂的代码块,这些代码块就被称为词法单元,我们以代码 var a = 2;为例,
var a = 2;
//这个代码通常会被拆分为 vara、=、2、;。

空格是否被当作词法单元,取决于空格在这门语言中是否具有含义,如果空格出现在字符串内,则空格会被认为是一个字符。

  • 解析/语法分析:这个过程会将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树 —— “抽象语法树”(AST); 这里面会有很多个节点组成。
  • 代码生成:这个过程就是将AST转换成可执行代码,这种被称为代码生成。简单来说就是有某种方法可以将var a = 2;的AST转化为一组机器指令,用来创建一个叫做a的变量,并将一个值储存在a中。

好,了解了什么是V8的编译了,那在JS代码内,V8引擎是如何对整体代码进行预编译的呢?请看接下来内容。

三、JS内的作用域

想了解V8引擎是如何对整体代码进行预编译的,我们需要先了解作用域这个概念。

作用域:就是一套规则,它是用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符进行变量查找的。

作用域包含全局作用域、函数作用域、块级作用域。 下面我们用一张图教你如何辨别作用域。 image.png

全局作用域

就是除了函数内部以外的其他部分,都属于全局作用域的。

函数作用域

函数内的区域就称为函数的作用域。由于代码执行是由上往下、由外到内执行的,所以,最先执行全局作用域内的内容,而函数作用域是全局作用域的子作用域。

还有一个比较神秘的就是 —— 块级作用域。块级作用域是ES6版本新增的作用域(新增了let和const关键词),因为块级作用域在特殊情况下才会产生,比如说:{} + let 就会产生,或者单独的 const 常量。

示例:

let定义变量a:

let 和 if 的 { } 组成块级作用域,导致if内的区域本该在全局作用域内的,从而变为全局作用域的子作用域,作用域的规则,内部作用域可以访问外部作用域,反之则不行。且此处的a未在全局作用域内定义,所以就会报错。

if(true){
    let a = 1;
    console.log(a);  //   1
}
console.log(a);

image.png

var定义变量a:

if(true){
    var a = 1;
    console.log(a);  //   1
}
console.log(a);  // 1

词法作用域

这个作用域很容易,接受就好,一个域所处的环境,是由函数声明的位置来决定的,而变量声明的地方,就是词法作用域。

function foo(){
    var a = 100
    function bar(){
        console.log(a);
    }
    return bar;
}
const baz = foo()
baz() //100

这里的函数变量bar就是在函数foo内部声明的,所以它的词法作用域就是指向foo。function foo(){ var a = 100 function bar(){ console.log(a); } return bar; }

四、全局的预编译

了解了作用域,我们知道函数作用域是全局作用域的子作用域,代码是执行和编译都是有先后顺序的,对于JavaScript来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内。 所以要看懂预编译的过程,还是先来了解它的编译过程。

全局的预编译过程

  1. 创建全局执行上下文对象GO
  2. 找变量声明,变量名作为GO的属性名,值为undefined
  3. 在全局找函数声明,函数名作为GO的属性名,值为函数体
  4. 执行时才赋值操作

下面举例说明全局预编译过程:

var a = 1
function foo(a){
    var a = 2
    function a(){}
    console.log(a)  // 2
}
foo(3)
//开始预编译
// GO = { //创建全局执行上下文对象GO
//     a : undefined,
//     foo: function,
// }
//执行后
// GO = { //给属性赋值
//     a : undefined 变为 1 ,
//     foo: function,
// }

五、函数中的预编译(v8引擎):

函数的预编译过程

  1. 先创建函数的执行上下文 AO 全称为(Activation Object)
  2. 找形参和变量声明,将形参和变量名作为AO的属性,值为undefined
  3. 将实参和形参统一
  4. 在函数体内找函数函数声明,将函数名作为AO的属性名,值赋予函数体
  5. 执行时才赋值操作

还是以全局预编译例子来解释。编译过程如上述步骤所示:

// 由于全局作用域代码的执行,对foo()进行了函数调用,由此产生了函数的预编译
//开始函数预编译
// AO = {
//   找变量声明和函数声明
//   形参 实参 函数 变量都是 a : un -> func;
//}
// 执行时进行赋值操作
//  AO = {
//   a : func -> 3 -> 2;
//}
//  所以这里最后会打印 2

所以,在执行console.log函数去打印a的值时,先在自身所在的作用域内寻找,也就是词法作用域,如果找到了就直接打印变量值,如果没找到,就会去外层作用域内寻找,如果一直到全局作用域内都没找到,则系统会抛出一个referenceError异常。

六、最后看一下欺骗词法:

JavaScript中的eval() 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在程序中,简单的讲它解析了字符串 ,把原本不属于这里的代码,搬到此处。所以叫做 欺骗词法。

function foo(a,str){
    eval(str);// 欺骗  !
    console.log(a,b); // 1,2

}
foo(1,'var b = 2')

除了这个eval(),另一个不常用的用来做欺骗词法作用域的功能是with关键字。

作用:用来修改对象属性值,但存在 bug -> 用with来修改对象的值时,如果对象没有这个属性,则with就会将修改的属性,泄露到全局变量。

image.png

到这里,基本的编译处理和作用域的知识你也就学的差不多了,这些都是我在学习过程中的一些理解和看法,希望能给正在学习JavaScript的你有一些帮助。与此同时,动动你发财的小手点个赞吧,你的鼓励就是我最大的支持哦