Js中的预编译

什么是预编译?

    引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。

1.预编译什么时候发生

  预编译分为全局预编译和局部预编译,全局预编译发生在页面加载完成时执行,而局部预编译发生在函数执行的前一刻。

  预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 。只有在解释执行阶段才会进行变量初始化 。

  目的:定义作用域中的初始化词法环境、减少运行时报错

2.预编译前奏

  一切声明的全局变量和未经声明的变量,全归window所有。 

  下面这个函数里面只有一个连等的操作,赋值操作都是自右向左的,而b是未经声明的变量,所以它是归window的,我们可以直接使用window.b去使用它。

function test(){
	// 这里的b是未经声明的变量,所以是归window所有的。
	var a = b = 110;
        console.log(a,b);
}
test();
console.log(a,b);//报错

3.预编译步骤 

  首先JavaScript的执行过程会先扫描一下整体语法语句,如果存在逻辑错误或者语法错误,那么直接报错,程序停止执行,没有错误的话,开始从上到下解释一行执行一行。

  1.全局编译的步骤

  • 生成GO对象 GO{}(global object) 这个GO就是window
  • 将全局的变量声明(的名)储存一GO对象中,value为undefinde
  • 将全局的函数声明的函数名作为go对象中的key,函数整体内容为value储存到go对象中

  2.局部编译的步骤    

  • 执行前的一瞬间,会生成一个AO(action object)对象
  • 到函数体作用域里找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined
  • 将实参和形参统一
  • 分析函数声明,函数名作为AO对象的属性名,值为函数体,如果遇到同名的,直接覆盖  

关于GO对象的例子:

全局预编译:在逐行执行;语法检测之前

   var a;
    function fun(){

    }
    function abc(){

    }
    function a(){

    }
    console.log(a);
    var a = 100;
    console.log(a);

  1. 会生成一个对象(GO),这个对象封装的就是作用域,称为GO(global object)。当全部挂载完成之后,然后代码在去逐行执行

GO{

}

  2. 分析变量声明(var)——变量作为GO对象的属性名,值为undefined

GO{
    a:undefined
}

  3. 分析函数声明(function)——函数名作为GO对象的属性名,值为函数体(如果遇到同名,直接覆盖)

GO={
  a:undefined;function a(){},
  fun:function fun(){},
  abc:function abc(){}                                                                                                                       
}

  4. 当走到某一行的时候;a产生了一次赋值;此时GO对象变成了:

 GO={
      a:100,
      fun:function fun(){}
      abc:function abc(){};                      
}

  5. 逐行执行(看着GO对象里面的执行)

输出结果:ƒ a() {}   	100	

关于AO对象的例子:

  概念:函数在每次运行时会重新创建函数内所有定义的变量,函数其实也是一种变量,因为它是变量名指向一个函数方法,所有的变量创建后加入到自身AO对象中,然后将在scope属性中加入自身AO对象。

  什么是AO:
    是函数执行前的一瞬间,生成一个AO对象(在函数执行前的一瞬间会生成自己的AO,如果函数执行2次,生成了两次AO,这两次的AO是没有任何关联)

function fn(a) {
    console.log(a);//1(第一个 console.log())
    var a = 123;
    console.log(a);//2
    function a() {}
    console.log(a);//3
    var b = function() {}//函数表达式
    console.log(b);//4
    function d() {}
    var d = a;
    console.log(d);//5
  }
  
  fn(1);
  
//输出结果:
[Function: a]
123
123
[Function: b]
123

  1. 创建AO对象

AO{

}

  2. 到函数体作用域里找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined

AO{
 a:undefined;
 b:undefined;
 d:undefined;
}

  3. 将实参和形参统一

AO{
 a:undefined; 1;
 b:undefined;
 d:undefined;
}
  1. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体
AO{
 a:undefined; 1; function (){ } 
   b:undefined;
 d:undefined; function() { }
}

  逐行执行(看着AO对象里面的执行)

第一个console.log:此时函数的执行环境内的变量为

AO{
 a:undefined; 1; function (){ }
 b:undefined;
 d:undefined; function() { }
}

第二、三、四、五个console.log:此时函数的执行环境内的变量为

AO{
 a:undefined; 1; function (){ }; 123;
 b:undefined;
 d:undefined; function() { }
}

  解释:当函数调用时,创建函数执行上下文,然后根据实参填充arguments对象,即:形参var a = arguments[0],然后根据函数内的函数声明将函数名a进行提升,此时会发现函数名和形参名发生了冲突,由于形参与arguments中的数据共享状态,所以接下来后提升的函数名a内存放的函数引用地址会将前面定义的形参名a指向的arguments[0]直接覆盖掉

  函数执行完毕,销毁AO对象。

在预编译这个过程中,首先将变量声明及函数声明提升至当前作用域的顶端,然后在进行接下来的处理,下面通过示例看下变量提升和函数提升

变量的提升:

示例1:

function hoistVariable() {
    if (!foo) {
        var foo = 5;
    }

    console.log(foo); // 5
}
hoistVariable();

  预编译之后

function hoistVariable() {
    var foo;

    if (!foo) {
        foo = 5;
    }

    console.log(foo); // 5
}

hoistVariable();

  引擎将变量声明提升到了函数顶部,初始值为undefined,自然,if语句块就会被执行,foo变量赋值为5

示例2:

var foo = 3;

function hoistVariable() {
    var foo = foo || 5;

    console.log(foo); // 5
}

hoistVariable();

  foo || 5这个表达式的结果是5而不是3,虽然外层作用域有个foo变量,但函数内是不会去引用的  

  预编译之后:

var foo = 3;

function hoistVariable() {
    var foo;

    foo = foo || 5;

    console.log(foo); // 5
}

hoistVariable();

函数的提升:

示例1:

function hoistFunction() {
    foo(); // 2

    var foo = function() {
        console.log(1);
    };

    foo(); // 1

    function foo() {
        console.log(2);
    }

    foo(); // 1
}

hoistFunction();

  第一次调用时实际执行了下面定义的函数声明,然后第二次调用时,由于前面的函数表达式与之前的函数声明同名,故将其覆盖,以后的调用也将会打印同样的结果  

  预编译之后

function hoistFunction() {
    var foo;

    foo = function foo() {
        console.log(2);
    }

    foo(); // 2

    foo = function() {
        console.log(1);
    };

    foo(); // 1

    foo(); // 1
}

hoistFunction(); 

示例2:

var foo = 3;

function hoistFunction() {
    console.log(foo); // function foo() {}

    foo = 5;
    
    console.log(foo); // 5

    function foo() {}
}

hoistFunction();
console.log(foo);     // 3

  可以看到,函数声明被提升至作用域最顶端,然后被赋值为5,而外层的变量并没有被覆盖  

  预编译之后:

var foo = 3;

function hoistFunction() {
   var foo;

   foo = function foo() {};

   console.log(foo); // function foo() {}
   
   foo = 5;

   console.log(foo); // 5
}

hoistFunction();
console.log(foo);    // 3

  函数的优先权是最高的,它永远被提升至作用域最顶部,然后才是函数表达式和变量按顺序执行

posted @ 2021-12-24 16:42  Judicious  阅读(630)  评论(0编辑  收藏  举报