不同作用域下的变量提升和函数声明提升

410 阅读7分钟

       变量提升和JavaScript的编译过程密切相关:JavaScript和其他语言一样,都要经历编译和执行阶段。在这个短暂的编译阶段,JS引擎会搜集所有的变量声明,并且提前让声明生效。而剩下的语句需要等到执行阶段、等到执行到具体的某一句时才会生效。这就是变量提升背后的机制。

变量提升:把变量的声明提升到当前作用域的最上面,不包括变量的赋值

函数提升:把函数的声明整体提升到当前作用域的最上面,不包括函数的调用,并且函数的提升优先级大于变量提升。

var声明的变量提升是不存在块级作用域的,函数提升存在块级作用域

关于第三点我们会重点讲解。我们从以下两个方面来看,

  • 全局作用域或函数作用域下的变量提升

在ECMAScript6之前,JS引擎用var关键字声明变量。在 var时代,不管变量声明是写在哪里,最后都会被提到作用域的顶端。

1.在全局作用域

var声明的代码很简单我们直接跳过,我们直接验证一下他们的优先级。

   console.log(window.a, a); //function a(){} function a(){}
   var a = 10;
   function a() {};
   console.log(window.a, a); //10  10

在不同的地方都打印a,通过代码我们可以看到,在全局下,通过var 和function声明的变量都会挂载在window。第一次打印出来的是函数a,而不是undefined,就可以证明函数的提升是高于变量声明提升的,并且后来对a的赋值会将前面的函数声明给覆盖。

2.在函数作用域

   function test() {
    console.log(window.a, a)//undefined  function a(){}
    var a = 10;
    console.log(window.a, a)//undefined  10
    function a() {}
    console.log(window.a, a)//undefined  10
   }
   test()

在函数作用域下,var定义a和function定义的a都是局部变量,不会挂载在window上。第一次打印的a是函数a,因为函数声明会提前,且优先级比变量声明高,并且后来对a的赋值会将前面的函数声明给覆盖。

  • 块级作用域中的变量声明和函数声明

1.使用默认声明的变量(隐式声明)

   console.log(window.a,a); //undefined   a is not defined
   {
    //console.log(window.a,a);//undefined  a is not defined
    a = 10;
    console.log(window.a, a) //10 10
   }
   console.log(window.a, a); //10 10

隐式声明没有变量提升,只有执行了隐式声明代码,声明的变量才会挂载到window上。

2.使用var 声明的变量

   console.log(window.a, a); //undefined  undefined
   {
    console.log(window.a, a); //undefined  、undefined
    var a = 10;
    console.log(window.a, a) //10 10
   }
   console.log(window.a, a); //10 10

可以看到var 声明的变量提升是不存在普通的块级作用域

3.块级作用域中的函数声明(重点

   console.log(window.a,a);//undefined undefined
   {
     console.log(window.a,a);//undefined function a(){}
     function a(){};
     console.log(window.a,a)//function a(){} function a(){}
   }
   console.log(window.a,a);//function a(){} function a(){}

回到第一个例子,如果块级作用域里面是一个默认变量,则第一行代码会直接报错,而函数声明在块外面则会打印undefined,这就说明**函数声明的提升存在块级作用域,只有在块内的顶部才可以打印出整个函数。**那为什么块外的a不会报错呢?

我们可以看一看阮一峰老师的这篇文章的描述

可以的得出结论:(1)块内的函数声明会提升到块内的顶部,同时也会在全局作用域用var声明一个同名的变量,初始值为undefined

第二个console似乎为啥window.a是undefiend,而a是有值呢? 再来回想下阮一峰老师的文章提到,函数声明会提升到块级作用域的顶部,那此时执行第二个console的时候在块内的顶部其实已经有了声明,所以此时a有值,而此时window.a没有值,通过断点发现,当执行了函数a这个代码后,window.a就有值了。所以第二个结论:(2)只有执行函数声明这段语句时才会影响外面的那个同名变量,并且将块内a的值赋予给外面的同名变量。

既然说块内的函数声明也会在外部类似于var 声明一个变量,那我们把它们放到一起看看

   console.log(window.a, a); //undefined undefined
   var a = 10;
   console.log(window.a, a); //10 10
   {
    console.log(window.a, a); //10  ƒ a(){}
    function a(){}
    console.log(window.a, a); //ƒ a(){} ƒ a(){}
   }
   console.log(window.a, a); //ƒ a(){} ƒ a(){}

从第三次的输出看,window上的a为10是因为var的原因,然后块内的a的值是函数,是因为函数会提升在块内顶端,这些没问题。当执行了函数声明的代码后,第四次输出,全局上的a果然由10变成了函数。这恰好完全证实了我们前面的两个结论,因为块内声明的函数会在全局类似于var声明一个变量a,所以当执行了函数声明的代码块,本来a=10是因为var定义的,现在却变成了函数。

4.块级作用域中既有函数声明又有默认变量声明(隐式声明)

   console.log(window.a, a); //undefined undefined
   {
    console.log(window.a, a); //undefined function a(){}
    function a() {};
    console.log(window.a, a); //function a(){} function a(){}
    a = 10;
    console.log(window.a, a); //function a(){}  10
   };
   console.log(window.a, a); //function a(){}  function a(){}

经过第三节的知识铺垫,对于代码前三次的打印,没有什么问题。然后我们也知道默认变量a本应该也会挂载在window上的,第四次打印的a=10,而window.a为函数a呢?此时其实a = 10这行代码不是隐式变量,因为a已经被函数定义过了,那a = 10也就是对之前定义过的变量赋值了,所以代码块内打印a的值是10。

既然这样,为什么a重新的赋值不会同步到window上呢?我们再看一下刚刚第三节时候的第二个结论这个块外的全局同名变量的赋值时机是执行完块内那行函数声明语句后才赋值,所以可以先猜想,因为外面那个变量是因为函数引起的,只有当执行函数代码的时候,a的值才会赋值给外面呢?我们按照这个猜想修改代码

   console.log(window.a, a); //undefined undefined
   {
    console.log(window.a, a); //undefined function a(){}
    function a() {};
    console.log(window.a, a); //function a(){} function a(){}
    a = 10;
    console.log(window.a, a); //function a(){}  10
    function a(){}
    console.log(window.a, a); //10 10
   };
   console.log(window.a, a); //10 10

果然在a=10后输出是和刚刚一样的,块内的a为10,全局的a是函数a。(其实这两个代码都是差不多的,因为函数a会被提升到块级的顶部,所以块内的a还是为10)当我们在后面再执行一次函数a时,a的值又赋值给了全局上。下面的代码其实和刚刚差不多,

   console.log(window.a, a); //undefined undefined
   {
    console.log(window.a, a); //undefined function a(){}
    a = 10;
    console.log(window.a, a); //undefined  10
    function a(){}
    console.log(window.a, a); //10 10
   };
   console.log(window.a, a); //10 10

所以可以得出结论:块级作用域函数只有执行函数声明语句的时候,才会重写对应的全局作用域上的同名变量。

通过下面这道题目,我们可以更加的巩固一下

   console.log(window.a, a); //undefined  undefined
   {
    console.log(window.a, a); //undefined  ƒ a() {}
    function a() {};
    console.log(window.a, a); //ƒ a() {}   ƒ a() {}
    a = 10;
    console.log(window.a, a); //ƒ a() {}   10
    window.a = 20;
    console.log(window.a, a); //20  10
    function a() {};
    console.log(window.a, a); // 10  //10
   }
   console.log(window.a, a); // 10  //10

中途我们将window上的a重新赋值了20,但是通过再次执行函数a的声明代码的时候,window上的a此时和块内的a又同步了。这时也再次证明了我们的前面的第二个结论。

  • 思考

    { var a = 10; function a(){} }

在var a = 10;这里会直接报错了,因为var和function定义的变量都会在块外声明一个变量,而且函数的优先级大于变量。所以这里不允许再用var了,问题就处在function定义的这个变量为什么和var定义的有冲突呢?路过的大佬可以帮我解释一下。

我们最后看看阮一峰的文章

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。