研究js的块级作用域中的变量声明和函数声明

7,877 阅读8分钟

昨天晚上在沸点看到一个小哥发了个沸点,代码很简洁,但是可是弄晕了我,评论区也是很热闹,我没事就研究了下,自己理解了下,感觉差不多可以解释通了。先来看那是什么样的代码吧

就是这个几行代码,反正我看了是不太明白为什么这样子,另外如果是强类型语言的程序员看了估计会说这个编译都不会通过吧,类型都不匹配怎么赋值啊,haha

以下所有代码都会讨论关于块级作用域的知识,不懂得可以先恶补阮一峰老师的文章 ES6 的块级作用域

另外我所提到的默认变量都指的是没有用var ,let,const关键词声明的变量

本文代码测试均在谷歌浏览器测试,支持es6语法,其余浏览器执行结果可能会有所不同,不代表所有浏览器的结果

块级作用域内的默认变量

我们来研究下块级作用域内的默认变量

运行结果是index.html:14 Uncaught ReferenceError: b is not defined 对于我来说有点震惊,b不是默认相当于var了吗?事实并非如此,我们根据事实可以得出结论,块级作用域内的变量声明不会提升到全局作用域的顶层,我们查看window也发现window并没有这个属性。再看下面图

报错:index.html:16 Uncaught ReferenceError: b is not defined

运行成功,输出 50,通过断点调试我发现,一旦 b =50执行后window上就有b这个属性了

无报错,第四行和第六行都输出了50,这说明块内定义的默认变量和块外访问的都是一个,也就是说块内定义的默认变量只有等到定义默认变量的那行代码执行后才会往window上挂载属性,这个行为跟var声明变量有点区别,我们来看下如果在块内用var来声明b是什么结果

输出是

可以看到在块内用var声明变量后行为符合我们的一般认知,b被提升到全局,一开始是undefined,直到执行赋值代码后才有值,var还是那个var,从来没变过,哈哈

小结:

  • 在块级作用域内部声明的默认变量(不适用let,var,const修饰),只有等到执行过你定义那个变量的那行代码后才可以访问,才给window赋值这个属性,在那行代码之前访问会报错
  • 块内的 默认变量依旧是全局变量
  • 在块内的默认变量没执行之前不可以访问这个变量

块级作用域内的函数声明

在块级作用域内部的函数声明很是诡异,直接看代码

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

输出 undefined 还记得块内的默认变量吗?它如果这样子写会报错的,但是函数声明就不会,因为正如阮一峰老师那篇文章所说

在块内的函数声明会提升到全局作用域顶部,并且类似var了一个同名的变量,那的确很类似var,默认值是undefined, 接着看

        console.log(`${window.a},${a}`);
        {
            console.log(`${window.a},${a}`);
           function a(){}
        }
        console.log(`${window.a},${a}`);

输出是

细心观察会发现,第一个console符合预期,第二个console似乎为啥window.a是undefiend,而a是有值呢? 再来回想下阮一峰老师的文章提高,函数声明会提升到块级作用域的顶部,那此时执行第二个console的时候在块内的顶部其实已经有了声明,所以此时a有值,而此时window.a没有值我猜测,因为window.a是提前在外面var a的那个a,而这个似乎也跟块内的默认变量有点相似就是块外的这个a只有等块内定义a的那行代码执行了才会赋值, 这个赋值行为跟块内直接访问a是不同的,直接访问a相当于访问块内提升到顶部的函数声明,执行的时候就有值了。

小结

  • 块内的函数声明会提升到块内的顶部,同时也会在全局作用域用var声明一个同名的变量,初始值为undefined
  • 这个块外的全局同名变量的赋值时机是执行完块内那行函数声明语句后才赋值
  • 块内的函数声明会提升到块内顶部,区别提升到块外,它并不会用var去声明一个同名的变量

块内同时有同名的默认变量和函数声明

到了最精彩的地方了

看代码1

       console.log(`${window.a},${a}`);
        {
            a = 50;
            console.log(`${window.a},${a}`);
           function a(){}
          
        }
        console.log(`${window.a},${a}`);

打印结果是

1.按照前两节的总结来,块内有函数声明,此时在全局var了一个同名的变量a,也等于window.a 2.第一行输出没毛病,var a嘛没赋值,默认是undefined 3.执行默认的变量 a =50,注意,此时执行后不要以为会像第一小节说的那样会给window挂载属性a,不会的,因为此时在块级作用域内部已经因为有了下面的函数声明,此时块级作用域顶部有了function a(){}的声明,你此时执行a = 50只是相当于赋值操作,没有任何声明,此时给a赋值的时候会查找作用域链有没有声明a,刚好函数声明提升到顶部了一个a,所以就把块内的a赋值为50,所以第二行打印winddow.a仍然是undefined,而a属于块内,此时被赋值为50了 4.按照第二节的总结,块外的那个跟块内函数声明同名的变量只有在函数声明那段代码执行后才会赋值, 所以最后一行代码执行时window.a已经被赋值了并且诡异的是块外的那个变量的值似乎跟块内函数声明的函数绑定着,当执行function(){}的时候会给外面的那个变量赋值,因为块内那个函数声明被a =50覆盖了,所以当执行完 function a(){}之后块外的那个变量就被赋值为50了,而非还是function(){}

看代码2

   console.log(`1 ${window.a},${a}`);
        {
           console.log(`2 ${window.a},${a}`);
           function a(){}
           a = 50;
           console.log(`3 ${window.a},${a}`);
        }
        console.log(`4 ${window.a},${a}`);

打印:

符合你的预期吗? 我觉得需要解释下第四个输出为什么window.a不是50,为什么没有被覆盖呢,按照上面所说的 a= 50的时候好像这个a跟外面的那个同名变量绑定着,其实这里你注意到function a(){}和 a= 50的顺序了吗? 是先写的function a(){},再写的a =50;我猜测只有执行function a(){}这段语句时才会影响外面的那个同名变量,其他时候不会影响,一旦执行过后,外面的那个变量的值就定死了,所以包括第三行,第四行输出的window.a已经不受a =50影响了,我又测试了下

 console.log(`1 ${window.a},${a}`);
        {
    
           console.log(`2 ${window.a},${a}`);
           function a(){}
           a = 50;
           function a(){} // 再增加个
           console.log(`3 ${window.a},${a}`);
        }
        console.log(`4 ${window.a},${a}`);

我在 a =50之后又写了个funaction a(){},正如我说,只有执行function a (){}的时候会触发改变外部的那个同名的全局变量,没执行一次就会触发改变外部的那个变量,并且对外部变量赋的值是跟内部同名的函数名绑定着。

小结

  • 块内的函数声明每次执行的时候都会给全局那个同名的变量赋值一次,并且,只有执行那个定义函数声明的代码才会触发赋值,你写的函数声明就相当于setter,每执行一次就给外部的那个同名的变量赋值一次
  • 如果块内同时有同名的函数声明和默认的变量声明,那给默认的变量赋值时其实相当于赋值给那个同名的函数,因为查找块内的作用域链时找到了,就不会往全局声明了

总结

js这门语言真的是很神奇的语言,几行代码都能让我琢磨半天,归根揭底还是弱类型导致的坑,你一个number类型的变量怎么可以赋值给function类型呢?如果能在编译时直接报错,估计就没这么多面试神题了,因为这就是不懂类型瞎赋值,但是弱类型也不是一无是处,这也是js牛逼的地方,另外都快0202了赶紧用typescript吧,用了一直爽!再也不写别人无法重构的代码了!

我也是初次研究这个,可能理解的有不到位,或错误的地方,如果有需要修改的地方请提出来,我不误人子弟

再次声明,所有代码均在谷歌浏览器上测试,其余版本和其他浏览器结果可能会有所不同!