JavaScript变量提升本质(预编译机制)详解

62 阅读8分钟

概念

JavaScript代码在执行前,引擎会先进行预编译,也称为变量提升。在这个阶段,它会将使用var声明的变量和函数声明提前到其所在的作用域的顶部进行定义(变量赋值留在原地),这就解释了为什么我们可以在声明之前使用变量(值为undefined)或调用函数。

全局作用域下的预编译

当一段JavaScript代码运行时,首先会进入全局作用域。全局下的预编译步骤如下:
1. 创建全局对象(Global Object, GO)
在浏览器中,这就是window对象。
2. 寻找变量声明:
扫描全局代码,找到所有使用var声明的变量,变将这些变量作为属性添加到GO中,其值初始化为undefined
「 注:let和const声明的变量也会被提升,但不会被初始化为undefined,且存在“暂时性死区”,在声明前访问会报错。经典的预编译讨论通常是聚焦于var和function 」
3. 寻找函数声明
扫描全局代码,找到所有函数声明(非函数表达式),并将函数名作为属性添加到GO中,值初始化为指向该函数在内存中的地址(通俗讲,就是函数本身)。
4. 开始执行代码
预编译完成后,代码开始逐行执行。此时,所有的变量和函数都已经存在于GO中。

全局预编译示例

  console.log(a); // 输出: undefined
  var a = 10;
  console.log(a); // 输出: 10

  function foo(b) {
    console.log(b);
  }
  foo(100); // 输出: 100

预编译过程(理论上的步骤):

  1. 创建 GO = { }。
  2. 找到 var a,在 GO 中创建属性:GO { a: undefined }。
  3. 找到函数声明 function foo(...),在 GO 中创建属性:GO { a: undefined, foo: function }。
  4. 开始执行代码。
    • 执行 console.log(a),此时 GO 中的 a 是 undefined,所以输出 undefined。
    • 执行 a = 10,将 GO 中的 a 赋值为 10。
    • 执行 console.log(a),此时 GO 中的 a 是 10,所以输出 10。
    • 执行 foo(100),调用函数。此时会为函数 foo 创建一个新的函数作用域,并开始函数自身的预编译过程。

函数作用域下的预编译

每当一个函数被调用时,在函数体代码执行之前,也会发生一个类似的预编译过程。步骤如下:

  1. 创建活动对象(Activation Object, AO):
    也可以理解为函数的局部作用域对象。
  2. 寻找形参和变量声明:
  • 将函数的形参作为属性添加到 AO 中,其值初始化为 undefined。
  • 扫描函数体内所有 使用 var 声明的变量,将这些变量名作为属性添加到 AO 中,其值同样初始化为 undefined。
  • 注意:如果形参和变量同名,则不会重新创建,直接覆盖值(详见示例)。
  1. 将实参赋值给形参: 将调用函数时传入的实参赋值给 AO 中对应的形参属性。
  2. 寻找函数声明: 扫描函数体内所有函数声明,并将函数名作为属性添加到 AO 中,其值初始化为指向该函数。如果函数名和变量或形参同名,则会覆盖掉之前的值。
  3. 开始执行函数体代码: 函数内部的预编译完成后,开始逐行执行函数体内的代码。

函数预编译示例

  function fn(a) {
    console.log(a); // 第一步:输出 function a() {}
    var a = 123;
    console.log(a); // 第二步:输出 123
    function a() {}
    console.log(a); // 第三步:输出 123
    var b = function() {}; // 这是函数表达式,不是函数声明,不会被提升
    console.log(b); // 第四步:输出 function() {}
  }
  fn(1);

让我们一步步分析函数 fn 被调用时的预编译和执行过程:
1. 创建 AO:
AO = { }
2. 找形参和变量声明:

  • 形参:a -> AO { a: undefined }
  • var 变量:var a(与形参同名,忽略),var b -> AO { a: undefined, b: undefined }

3. 将实参赋值给形参:
实参是 1 -> AO { a: 1, b: undefined }
4. 找函数声明: 找到 function a() {},将 AO 中的 a 覆盖 -> AO { a: , b: undefined }
预编译完成,此时的 AO 内容为:

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

5. 开始执行函数体代码:

  • console.log(a); // 从 AO 中查找 a,结果是函数,输出 function a() {}
  • a = 123; // 赋值操作,将 AO 中的 a 修改为 123
  • console.log(a); // AO 中的 a 现在是 123,输出 123
  • function a() {} // 这行在预编译阶段已经处理过了,执行阶段直接跳过
  • console.log(a); // AO 中的 a 仍然是 123,输出 123
  • b = function() {}; // 赋值操作,将 AO 中的 b 修改为这个匿名函数
  • console.log(b); // AO 中的 b 现在是函数,输出 function() {}

下面我们来做点题巩固一下吧

1. 题目一:

 function test(a, b){
   console.log(a); //1
   console.log(b); //function b(){}
   console.log(c); //undefined
   c = 0;
   console.log(c); //0
   var c;
   a = 3;
   b = 2;
   console.log(a); //3
   console.log(b); //2
   function b(){}
   console.log(b); //2
 }

 test(1)

预编译和执行过程:

  1. 创建AO
 AO = {}
  1. 找形参和变量声明
  • 形参:a,b
 AO{
   a:undefined, 
   b:undefined
 }
  • var变量:var c
 AO{
   a:undefined, 
   b:undefined,
   c:undefined
 }
  1. 将实参赋值给形参
    实参是1
 AO{
   a:1, 
   b:undefined,
   c:undefined
 }
  1. 找函数声明
    找到function b(){},将函数体复制给AO的b属性,预编译完成,此时的AO为:
 AO{
   a:1, 
   b:function b(){},
   c:undefined
 }
  1. 开始执行函数体代码
  • console.log(a);//AO中的a是1,输出1
  • console.log(b);//AO中的b是函数,输出function b(){}
  • console.log(c);//AO中的c是undefined,输出undefined
  • c = 0;//赋值操作将AO中c修改为0
 AO{
   a:1, 
   b:function b(){},
   c:0
 }
  • console.log(c);//AO中的c是0,输出0
  • var c;这行在预编译阶段已经处理过,执行阶段直接跳过
  • a = 3;//赋值操作将AO中a修改为3
 AO{
   a:3, 
   b:function b(){},
   c:0
 }
  • b = 2;//赋值操作将AO中b修改为2
 AO{
   a:3, 
   b:2,
   c:0
 }
  • console.log(a);//AO中的a是3,输出3
  • console.log(b);//AO中的b是2,输出2
  • function b(){}这行在预编译阶段已经处理过,执行阶段直接跳过
  • console.log(b);//AO中的b是2,输出2

执行代码最终输出的结果是:

  1
  function b(){}
  undefined
  0
  3
  2
  2

2. 题目二:

 function test(a, b){
   console.log(a); //function a(){}
   console.log(b); //undefined
   var b = 234;
   console.log(b); //234
   a = 123;
   console.log(a); //123
   function a(){}
   var a;
   b = function (){}
   console.log(a); //123
   console.log(b); //function (){}
 }

 test(1)

预编译和执行过程:

  1. 创建AO
 AO = {}
  1. 找形参和变量声明
  • 形参:a,b
 AO{
   a:undefined, 
   b:undefined
 }
  • var变量:var b,var a,此时AO中已经存在属性b,a就不用再创建了
 AO{
   a:undefined, 
   b:undefined
 }
  1. 将实参赋值给形参
    实参是1
 AO{
   a:1, 
   b:undefined
 }
  1. 找函数声明
    找到function a(){},将函数体复制给AO的a属性,预编译完成,此时的AO为:
 AO{
   a:function a(){}, 
   b:undefined
 }
  1. 开始执行函数体代码
  • console.log(a);//AO中的a是函数,输出function a(){}
  • console.log(b);//AO中的b是undefined,输出undefined
  • var b = 234;//赋值操作将AO中b修改为234
 AO{
   a:function a(){}, 
   b:234
 }
  • console.log(b);//AO中的b是234,输出234
  • a = 123;//赋值操作将AO中c修改为0
 AO{
   a:123, 
   b:234
 }
  • console.log(a);//AO中的c是123,输出123
  • function a(){}这行在预编译阶段已经处理过,执行阶段直接跳过
  • var a;这行在预编译阶段已经处理过,执行阶段直接跳过
  • b = function b(){};//赋值操作将AO中函数赋值给属性b
 AO{
   a:123, 
   b:function (){}
 }
  • console.log(a);//AO中的a是123,输出123
  • console.log(b);//AO中的b是function (){},输出function (){}

执行代码最终输出的结果是:

  function a(){}
  undefined
  234
  123
  123
  function (){}

3. 题目三:

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

预编译和执行过程:

  1. 创建GO
 GO = {}
  1. 查找变量声明
 GO = {
   test: undefined
 }
  1. 查找函数声明
  GO = {
    test: function test(test){
      console.log(test); 
      var test = 234;
      console.log(test); 
      function test(){}
    }
  }
  1. 开始执行代码
  • console.log(test); GO中的test是函数,输出function test(){}
  • test函数在预编译是已经处理过,直接跳过
  • 执行函数test(1),此时为函数test创建一个新的函数作用域,并开始了函数自身的预编译。
  1. 创建AO
 AO = {}
  1. 找形参和变量声明
  • 形参:test
 AO{
   test:undefined
 }
  • var变量:var test,此时AO中已经存在属性test就不用再创建了
  1. 将实参赋值给形参
    实参是1
 AO{
   test:1
 }
  1. 找函数声明
    找到function test(){},将函数体复制给AO的test属性,预编译完成,此时的AO为:
 AO{
   test:function test(){}
 }
  1. 开始执行函数体代码
  • console.log(test);//AO中的a是函数,输出function test(){}
  • var test = 234;//赋值操作将AO中test修改为234
 AO{
   test:234
 }
  • console.log(test);//AO中的test是234,输出234

至此函数作用域的预编译执行完,继续执行全局作用于的代码 var test = 123;

GO = {
 test:123
}

执行代码最终输出的结果是:

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

经过上面练习题,是否已经掌握了解答过程呢,下面的几题就不写详细的解题过程了,直接给出结果,自己去测验一下吧。
4. 题目四:

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

 var a;
 test()
 a = 10;
 console.log(a);
 console.log(c);

执行代码最终输出的结果是:

  undefined
  undefined
  234
  10
  234

5. 题目五:

 function bar(){
   return foo;
   foo = 10;
   function foo(){}
   var foo = 11;
 }

 console.log(bar()) 

执行代码最终输出的结果是:

  function foo(){}

6. 题目六:

  console.log(bar());
  function bar(){
    foo = 10;
    function bar(){}
    var foo = 11;
    return foo
  }

执行代码最终输出的结果是:

  11