变量声明提升

作者: 彬_仔 | 来源:发表于2016-11-08 21:32 被阅读0次

    变量声明提升是JS中一个基础的问题,同时也是对JS词法作用域认识的一个提升。在JS面试题中,关于变量声明提升的问题还是占了不少比例的,另外,在码代码的时候可能也无意间因为这个原因产生错误而头疼好久。还有一个需要注意的是ES2015中let、const声明的变量不具备变量声明提升。

    在《你不知道的JavaSript》上卷中,作者把变量声明提升这个问题比作“先有鸡还是先有蛋?”,我认为很形象。代码在执行的时候给人的感觉是一行一行的执行,这样可能比较符合我们的正常思维习惯,但是这实际上并不完全正确。为什么这样说呢?这就要引出JS在运行前其实有一个编译过程的这个问题,在编译阶段,JS引擎做了一些事使得代码并不是完全一行一行的执行了,而是将一些声明的代码顺序提前了,所以就产生了变量声明提升这个问题。

    编译原理

    想明白变量声明提升这个概念,JS编译原理必须清楚。
    有些小伙伴认为JS不是一门编译动态脚本语言吗?怎么会有编译过程,其实在没有接触《你不知道的JavaSript》上卷这本书之前我也这么认为,直到读了这本书我才对JS的作用域和变量声明提升以及闭包等问题有了更清楚的认识,所以这里先向小伙伴们推荐一下这本书。

    言归正传,接着说JS编译,JS的编译过程不是像其他语言的编译过程一样发生在构建之前的,大部分是在代码执行之前的几微米(甚至更短!)的时间内,那么这段时间内,编译器对我们的代码做了什么呢?我们通过一个例子来说明一下,比如var a =2;这条语句在会被JS引擎看成是两部分,分别为var a;a = 2。其中前一部分是发生在编译过程中,而第二部分发生在执行过程中。也就是说,编译的时候,JS引擎把我们对a的声明已经提前了。

    为了更好地说明编译器的编译过程,我在举一个例子:

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

    上面这段代码在执行的时候,JS引擎的工作过程是:

    1. 在编译阶段,首先遇到foo(2);一看这是个函数执行呀,这并不是我编译器的活呀,于是直接无视略过。
    2. 然后继续向下走,发现function,很明显是要声明一个函数(ES2015之前,声明变量只有var 和 function 这两个关键字,前者用于声明普通变量,后者用于声明函数或者方法),所以,JS引擎就会在当前作用域内的内存中开辟一块空间给foo;然后编译继续进行,这时候该对foo函数内部进行编译(从上到下一行一行编译),所以遇到形参a后,就在foo作用域的内存中为a开辟了一块空间,只不过此时a没有值,所以存的是undefined,继续向下走,没了-----结束。
    3. 现在开始执行阶段,首先遇到了foo(2);,开始干活:
      引擎:作用域,你见过foo没?
      作用域:见过,刚才编译器那小子刚声明了他,我给你。
      引擎:好的。那我来执行以下foo这个函数。
      引擎:作用域兄弟,你在foo中见过a吗?
      作用域:有,编译器也声明他了,给你。
      引擎:谢了哥们,我把2复制给他。

      .....

    我发现虽然是简单描述了一下JS引擎的编译过程,好像已经莫名其妙的把变量声明提升给讲完了(尴尬。。。)。

    变量声明提升

    本文是用来记录变量声明提升的,结果在第一小节就通过JS引擎的编译过程就给讲完了。。。。。。

    那这一小节就在再总结一下,顺便说一下函数优先吧!

    变量声明提升的原因

    1. JS代码在执行之前有一个极其短的编译过程。
    2. 在这个过程中,JS引擎为var、function声明的变量和函数在当前作用域中分配内存空间。函数内的变量和嵌套函数也是一样,只不过分配的内存是其父函数作用域内的。
    3. JS引擎在执行的时候,通过询问作用域来查找有无该变量或者函数,然后执行相关赋值或者函数执行等操作。

    函数优先

    相信已经说清楚变量声明提升的原因了,但是还要注意一点就是,在编译过程中,如果var和function声明的变量为同一个,则function声明的优先级高于var声明的。来看一个例子吧!

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

    上面代码最终输出结果是1,你猜对了吗?上面的编译执行过程为:

    1. 编译阶段,从上往下编译,首先遇到foo();,直接无视略过。
    2. 遇到var foo;,在当前作用域内为foo分配内存空间,继续向下编译。
    3. 遇到function foo(){....},发现已经在当前作用域中声明了该变量,但是此时是function,优先级明显高于var,所以当前作用域中的foo变为function。
    4. 继续向下编译,发现foo=....很明显是个赋值操作吗,这是引擎的事,无视。
    5. 执行阶段,首先就遇到了foo();,于是询问当前作用域内有无foo的声明,发现有,还正好是个函数,那就别废话了,直接执行吧!于是控制台打印了1。
    6. 继续向下执行,略过var foo;function foo(){....},遇到一个复制操作,那就先在问问当前作用域有没有foo变量,有就赋值,没有就在全局中声明一个foo变量(非严格模式下);
    7. 好了,到此结束。

    小结

    变量声明是一个很基础的JS知识点,但如果没有编译这一步,可能理解上不好理解,但是有了编译这个过程后,相信就很容易了。最后留下一道题,检验一下自己:

    var a = new Object();
    a.param = 123;
    
    function foo(){
      get = function(){
          console.log(1);
      };
      return this;
    }
    
    foo.get = function(){
      console.log(2);
    };
    
    foo.prototype.get = function(){
      console.log(3);
    };
    
    var get = function(){
      console.log(4);
    };
    
    function get(){
      console.log(5);
    }
    
    foo.get();
    get();
    foo().get();
    get();
    new foo.get();
    new foo().get();
    new new foo().get();
    

    很经典的一道考察变量声明提升和原型链,还有操作符优先级的题。

    相关文章

      网友评论

        本文标题:变量声明提升

        本文链接:https://www.haomeiwen.com/subject/jjxfuttx.html