JavaScript编译原理:编译器,引擎,作用域


首先,JavaScript确实是一门编译型语言,与C等典型编译型语言相比,区别在于JavaScript的编译过程(通常)是在实际执行前进行的,而且并不会产生可移植的编译结果。

一、Javascript编译过程

1. 分词与词法分析

把输入的字符串分解为一些对编程语言有意义的代码块(词法单元)。
这个过程会将字符串分割为有意义的代码块,这些代码块称之为词法单元。

例如变量的声明:

var a = 2;

这行代码会被分为以下词法单元:var、a、=、2(空格算不算词法单元取决于空格对于该编程语言是否具有意义);这些零散的词法单元会组成一个词法单元流(数组)进行解析。

2. 解析与语法分析

将上一步的词法单元集合分析并最终转换为一个由元素逐级嵌套所组成的代表了程序语法结构的树,称为抽象语法树(Abstract Syntax Tree,AST)AST在线解析工具

3. 代码生成

将上一步的AST转换为可执行代码。

二、JavaScript编译特点

由于编译就在代码执行前,所以JavaScript编译执行效率就比一般静态语言敏感得多,故而也非常复杂。
JavaScript在这部分做了许多优化:

  • 一是对语法分析和代码生成阶段进行优化(例如针对冗余元素进行优化),目的是提高编译后的执行效率。
  • 二是对编译过程进行优化(如JIT,延迟编译或者重编译),目的是缩短编译过程,保证性能最佳。

JavaScript 中的编译器、引擎和作用域

  • 编译器:负责语法分析和代码生成。
  • 引擎:负责整个过程中JavaScript的编译及执行过程。浏览器不同,其引擎也不同,比如Chrome采用的是v8,Safari采用的是SquirrelFish Extreme。
  • 作用域:负责收集并维护所有的标识符(变量)。

JavaScript编译过程具体分析

1. 一个具体的例子

var a = 2;

首先进行词法分析,然后将词法单元流交给编译器生成AST,再由编译器生成可执行的代码

  • 编译器遇到 var a;,编译器询问:同一作用域集存在同名变量 ? 忽略该声明,继续编译 : 要求作用域在当前作用域的集合生成一个名为a的新变量。
  • 编译器会为引擎的运行生成一系列代码,这些代码用于为变量a进行赋值操作。引擎会询问:当前作用域存在这个变量 ? 进行赋值操作 : 查找这个变量(从当前作用域向上查找,直到全局作用域,如果还是没有,就会抛出一个异常)。
  • LHSRHS,当引擎执行编译器给的代码(赋值操作)时,会通过查找这个变量来判断这个变量是否已经声明,这个过程需要作用域的协助,而查找的方式分为两种:LHS(“赋值操作的目标是谁”)和RHS(”谁是赋值操作的源头“)。
    • LHS:赋值操作的左侧,试图查找到变量的容器本身,从而可以对其赋值,即找到复制操作的目标。

    • RHS:另外一种查找,可以简单理解为复制操作的右侧,其查找目标为取到目标的源值,即找到这个变量具体的值而非容器。

      LHS与RHS举例:

      	var a;  //LHS引用
          a = 2;  //LHS引用
          alert(a);  //RHS引用
          
           /** 
            * 这段代码块既有LHS引用也有RHS引用,
            * 2被当作函数参数传递给foo()时,
            * 2会被分配给变量a(a = 2);
            */
          function foo(a){
            alert(a);
          }
          foo(2);
      

      区分RHS和LHS也很重要,尤其分析异常时。例如下面:

      function foo(a){
         alert(a + b); 
         b = a;
       }
       foo(2);
      

      第一次对b进行RHS查询会查询不到这个变量,因为它是一个未声明的变量,在所有作用域都无法找到var b;;此时引擎会抛出一个异常(ReferenceError)。在非严格模式下,当引擎进行LHS查询查询不到某个变量时,全局作用域会创建一个同名的变量交给引擎,当然这个变量具有全局作用域;而在严格模式下,引擎会抛出ReferenceError异常。总结一下就是:

    • RHS未找到:引擎会抛出错误RefrenceError

    • LHS未找到:引擎(或引擎中的编译器)会帮你在顶层作用域声明一个具有该名称的变量。(严格模式除外)。

      举个例子:
      var a;// LHS 寻找a,未找到,通知作用域声明一个新变量,命名为a
      a=2;// LHS 找到a并给其赋值2
      console.log(a);//RHS找到a的值2,并将其输出
      

2. 关于词法作用域

JavaScript其根据一套规则来管理变量的查找与引用,词法作用域就是其使用的规则,在编译器进行词法化时,会根据你写代码时将变量和块作用域写在哪里,来决定规则的内容。这其中又包含了块作用域这个概念,不展开讲,只要记住ES6之前没有块作用域,只有函数有作用域,即:函数内部是一个独立的块作用域。(有个特例:catch语句块内也是独立的作用域。)

3. 关于变量提升

明白了编译器和引擎执行之间的分工,其实你应该就不会觉得变量提升是如此之诡异了,因为引擎拿到代码的时候,编译器已经做了一些转换,编译器干嘛要干这个事情?因为它要在第一步就找到所有的声明,并且用合适的作用域将他们关联起来,这也正是词法作用域的核心。表现为: 包括变量和函数在内的所有声明都会在当前块作用域内被首先处理,即类似于提升到最前面声明,但是复制处理操作因为是在执行阶段,因此编译阶段他们原地待命等待执行。

  • 变量和函数在内的声明都在任何代码执行前被处理。声明操作在编译阶段时进行的,而赋值操作是在等到执行阶段才执行。

    //代码块1
    var a = 2;
    alert(a);  //  输出2
    
    //代码块2 
    b = 2;
    var b;
    alert(b);  //输出2
    
    //代码块3
    alert(c);  //输出undefined
    var c = 2;
    
    //代码块4
    var d;
    alert(d); //输出undefined
    d = 2;
    

    代码块2,4等价于代码块1,3(除了变量名不同,内存地址不同);这个过程就好像变量和函数声明的代码被移动到了最上面,这个过程就叫提升

  • 函数声明可以提升,函数表达式不能提升

    //函数声明可以提升
    foo();  // 输出2;
    function foo(){
      alert(2); 
    }
    //函数表达式不可提升
    bar();  // TypeError
    var bar = function f1(){
      alert(2);
    }
    
  • 函数声明优先于变量声明提升,出现在后面的函数声明可以覆盖之前的声明。

    foo();  // 输出3
    function foo(){
      alert(1);
    }
    var foo = function bar(){
      alert(2);
    }
    function foo(){
      alert(3);
    }
    

    等价于

    var foo;
    foo = function () {
    	alert(1)
    }
    foo = function () {
    	alert(3)
    }
    foo()
    foo = function bar() {
    	alert(2)
    }
    

三、三兄弟合作

第一版

下面我们以一个最简单的例子var a = 2;来进行分析:

  1. 编译器出马,先进行词法分析,将该赋值操作拆分:var a; a=2;。第一步 var a,编译器可以处理,他会先询问变量管家——作用域:存在一个该名称的变量 ? 继续编译 : 通知作用域声明一个新变量,命名为a。
  2. 编译器继续为引擎进行代码生成,这些代码主要用来处理a=2这个赋值操作。
  3. 引擎拿到可执行代码,然后询问作用域:当前有一个叫a的变量吗 ? 使用这个变量,赋值给他 : 继续往上级作用域查找。如果到根作用域仍然找不到,引擎直接报错抛异常。

第二版

有了上面的基础知识,我们把三兄弟的合作再细化一下,例子也升级一下,用上面赋值并输出的例子。

  1. 编译器:作用域,我需要对a进行LHS查找,你见过么?
  2. 作用域:我这找到根都没看到啊,要不咱声明一个吧!
  3. 编译器:好,建好了,那我生成代码了,引擎,给你你要的代码。
  4. 引擎:收到,咦,需要一个a啊,作用域,帮我LHS找一下有没有?
  5. 作用域:找到了,编译器已经帮忙声明了。
  6. 引擎:好的,那我对它赋值。
  7. 引擎:作用域,不要意思,我碰到一个console,需要RHS引用。
  8. 作用域:找到了,是个内置对象,拿走不谢。
  9. 引擎: 好的作用域,对了能在帮我确认一下a的RHS么?
  10. 作用域:确认好了,没变,拿去用吧,他的值是2
  11. 引擎:好咧,我把2传递给log(…)

四、关于作用域

作用域范围

传统的类C的语言作用域是块级作用域block-level scope,一个花括号就是一个作用域,而对于JavaScript来讲,作用域是函数级function-level scop。JavaScript语言的作用域仅存在于函数范围中。

全局作用域

在JavaScript代码中的任何地方都有定义的变量被称为全局变量,其也拥有全局作用域。一般来说,不在任何函数体内定义的变量以及未定义就直接赋值的变量拥有全局作用域。事实上,JavaScript默认拥有一个全局对象window,声明一个全局变量,就是为window对象的同名属性赋值。如下面代码所示。

function fun1(){ }
var a = 1;
console.log(window.a);//1
console.log(window.fun1); // function fun1(){}

函数级作用域

在JavaScript中,任何定义在函数体内的变量或者函数都将处于函数作用域中,这些变量也无法被在函数外部使用。函数内部声明的所有变量在函数体内始终是可见的,在JavaScript函数定义中,JavaScript在预编译阶段中会先扫描整个函数体的语句,将所有声明的变量“提升”到函数顶部。

function test(o) {
  var i = 0; //  i在整个函数体内均是有定义的
  console.log(j); //j在里面有定义,但是没有赋值
  console.log(k); //k在里面有定义,但是没有赋值。
  if (typeof o == "object") {
    var j = 0;
    for (var k = 0; k < 10; k++) {
      console.log(k);
    }
    console.log(k); // 输出10;
  };
  console.log(j); //若o为对象类型,则为0;否则为undefined
};

当函数体内局部变量和函数体外的变量重名的话,内部局部变量将会遮盖同名的全局变量。

var scope = "global";

function f() {
  console.log(scope); //undefined
  var scope = "local";
  console.log(scope); //local;
}
//如前面所说的,“变量提升”,所有的变量将会预先编译,且赋值为undefined。

JavaScript函数内的嵌套函数可以访问外层函数的变量,但是外层函数访问不了嵌套函数的变量。

var a = 1;

function fun4() {
  var b = 1;
  console.log(a);
  console.log(c); //报错 
  function fun5() {
    console.log(a); //1
    console.log(b); //1
    var c = 3;
  }
}
fun4();

事实上,无论是函数作用域中的覆盖问题还是变量的访问权限,起作用的是作用域链。

作用域提升

变量提升

对JavaScript解释器而言,所有的函数和变量声明都会被提升到最前面, 并且变量声明永远在前面,赋值在声明过程之后。比如:

var x = 10;
function x(){};
console.log(x); // 10

实际上被解释为:
var x;
x = function x(){};
x = 10;
console.log(x); // 10

函数提升

函数的声明方式主要由两种:声明式和变量式。声明式会自动将声明放在前面,并且执行赋值过程。而变量式则是先将声明提升,然后到赋值处再执行赋值。比如:

function test() {
  foo(); // TypeError "foo is not a function"
  bar(); // "this will run!"
  var foo = function () { // function expression assigned to local variable 'foo'
    alert("this won't run!");
  }
  function bar() { // function declaration, given the name 'bar'
    alert("this will run!");
  }
}
test();

实际上等价于:

function test() {
  var foo;
  var bar;
  bar = function () { // function declaration, given the name 'bar'
    alert("this will run!");
  }
  foo(); // TypeError "foo is not a function"
  bar(); // "this will run!"
  foo = function () { // function expression assigned to local variable 'foo'
    alert("this won't run!");
  }
}
test();

主要注意的地方:带有命名的函数变量式声明,是不会提升到作用域范围内的,比如:

var baz = function spam() {};
baz(); // vaild
spam(); // ReferenceError "spam is not defined"

引用地址:https://www.jianshu.com/p/5ebf2ad6def2

猜你喜欢

转载自blog.csdn.net/weixin_44116302/article/details/95995443