JS----预编译及变量提升详解

5,392 阅读4分钟

前言

JS属于解释型语言,在执行过程中顺序执行,但是会分块先预编译然后才执行。因此在JS中存在一种变量提升的现象。搞懂预编译环节,变量提升自然而然也就懂了。本文讲围绕以下几点进行介绍(变量提升会穿插在其中讲解):

  • 预编译执行步骤
  • 示例演示

预编译执行步骤

预编译发生在函数执行的前一刻,过程如下:

  1. 创建AO对象,执行期上下文(后面更新关于执行期上下文详解)。
  2. 寻找函数的形参和变量声明,将变量和形参名作为AO对象的属性名,值设定为undefined.
  3. 将形参和实参相统一,即更改形参后的undefined为具体的形参值。
  4. 寻找函数中的函数声明,将函数名作为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. 创建AO对象
AO{
    //空对象    
}
  1. 找形参和变量声明
AO{
    a : undefined,
    b : undefined
}
  1. 形参和实参相统一
AO{
    a : 1,
    b : undefined
}
  1. 找函数声明
AO{
    a : function a(){},
    b : undefined,
    d : function d(){}
}

预编译环节就此结束,此时的AO对象已经更新为:

AO{
    a : function a(){},
    b : undefined,
    d : function d(){}
}

函数开始逐行顺序执行:

 function fn(a){
    console.log(a);// 输出functiona(){}
    var a = 123;//执行到这里重新对a赋,AO对象再一次更新
    console.log(a);// 输出123
    
    function a(){};//预编译环节已经进行了变量提升,故执行时不在看这行代码
    console.log(a);// 输出123
    
    var b = function(){};//这个是函数表达式不是函数声明,故不能提升,会对AO中的b重新赋值
    console.log(b);//输出function(){}
    
    function d(){};
 }

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

我们再来看几个例子,熟悉函数的预编译过程。

示例一:

function test (a,b){
    console.log(a);
    c = 0;
    var c;
    a = 3;
    b = 2;
    console.log(b);
    function b(){};
    function d(){};
    console.log(b);
} 
//调用函数
test(1);

它的AO创建过程如下(此处省略创建空AO对象的部分,下文同):

AO1{
    a : undefined,
    b : undefined,
    c : undefined
}

AO2{
    a : 1,
    b : undefined,
    c : undefined
}

AO3{
    a : 1,
    b : function b(){},
    c : undefined,
    d : function d(){}
}

至此预编译环节完成,开始执行:

function test (a,b){
    console.log(a); //输出1
    c = 0; //给AO对象中的c重新赋值0
    var c;//预编译环节变量提升,不再读此行代码
    a = 3;//给AO对象中的a重新赋值3
    b = 2;//给AO对象中的b重新赋值2
    console.log(b);//输出2
    function b(){};//预编译环节变量提升,执行时不再读这行代码
    function d(){};//预编译环节变量提升,执行时不再读这行代码
    console.log(b);//输出2
} 
//调用函数
test(1);

示例二:

这个例子中我们引入全局对象GO。GO与AO的过程类似

function test(){
    var a = b = 123;
}
test();

此函数的执行过程:先把123赋给b,再声明a,再把b赋给a。此时变量b未经声明就赋值,为全局变量。预编译环节如下:

GO1{
    b : undefined
}
AO1{
    a : undefined
}


GO2{
    b : 123;
}
AO2{
    a : 123;
}

示例三 :

console.log(test);
function test(test){
   console.log(test);
   var test = 234;
   console.log(test);
   function test(){};
}
test(1);
var test = 123;

我们来看它的预编译过程:

//执行前(页面加载完成时)生成GO对象
GO1{
    test : undefined
}
GO2{
    test : function(){}
}

//输出 function test(){...}

//执行test()前生成它的AO对象
AO1{
    test : undefined
}
AO2{
    test : 1
}
AO3{
    test : function test(){}
}

//预编译结束开始执行test(1);
AO4{
    test : 234
}
//输出234

示例四:

function demo(){
    console.log(b);
    if(a){
        var b = 100;
    }
    console.log(b);
    c = 234;
    console.log(c);
}
var a;
demo();
a = 10;
console.log(c);

我们来看它的预编译过程:

//首先是全局对象GO 
GO1{
    a : undefined
}
G02{
    a : undefined,
    demo : function demo(){}
}
//执行demo()前预编译,由于demo中的c未声明就使用故为全局对象

//输出undefined
GO3{
    a : undefined,
    demo : function demo(){}
    c : undefined
}
//此时a还是undefined,故不执行if()代码块
//输出还是undefined
GO4{
    a : undefined,
    demo : function demo(){}
    c : 234;
}
//输出234
GO5{
    a : 10,
    demo : function demo(){}
    c : 234;
}
//输出234