zoukankan      html  css  js  c++  java
  • 关于提升和作用域的一道有趣的题目

      前几天看到了一道慕课网上推荐的有趣的js题目,来自手记《99%的人都会答错的js面试题(你会吗?)》,作者是King。今天我专门研究了一下这道题,下面总结一下我目前对这道题目的理解。首先我对他的原题稍加改动引用如下,我会在后面解释作出改动的原因。
                
    function Foo(){ 
        getName=function(){console.log(1);}; 
        return this;
    }
    Foo.getName=function(){console.log(2);};
    //Foo.prototype.getName=function(){console.log(3);};//注释掉原文这句话,因为涉及到原型,本文暂不讨论
    var getName=function(){console.log(4);};
    function getName(){console.log(5);}
    Foo.getName();//第一问输出2
    getName();//第二问输出4
    Foo();//第三问,有所改动,原问可以在文末我提供的原文链接里找到,但是原问存在一些问题控制台会提示出错
    getName();//第四问输出1
      因为我这几天在看一本叫做《你不知道的JavaScript(上卷)》的书,书中讨论了作用域以及变量提升问题,刚好发现这道题中也涉及到该知识点,所以单独将原题的前四问拿出讨论,原题目中还有另外三问,分别涉及的其它知识点,本文暂不做讨论。手记原文中作者对该题的论述很详细,有兴趣的童鞋可以去看看,本文只探讨我自己通过所看的书籍提供的信息对这道题的理解。
     
      根据书中第4章“提升”所讲,虽然直觉上认为javascript代码是由上到下一行一行依次执行的,然而引擎会在解释代码前先对其编译,编译的第一步就是找到所有声明并用合适的作用域进行关联。因此,正确的思路是无论声明出现在何处,都将在代码被执行前在各自所处的作用域中被首先进行处理,即“提升”。书中提到提升有如下几个规则:
      1.只有声明本身(两种声明:变量声明与函数声明)会被提升,而赋值操作和其它运行逻辑会留在原地。
      2.每个作用域都会在各自的作用域内进行提升操作。
      3.在两类声明提升的过程中,函数提升优先,你可以理解为提升后所有的函数声明都在变量声明之前。
      4.用变量声明的函数表达式并不会被提升,即使是具名的函数表达式名,其名称标识符在赋值之前也无法在所在作用域使用。
      5.对于重复的声明,重复的var声明会被忽略,但出现在后边的函数声明是会覆盖前面的。
      6.一种不可靠行为:函数声明不会被条件判断所控制。这在js未来的版本中可能改变,应该尽可能避免在块内部声明函数。
      总结后两条来说,在同一作用域重复定义是件非常糟糕的事,会产生各种奇怪问题,应该尽量避免这样做。
     
      根据以上规则,我们对这道题进行第一步处理,即声明提升,结果如下:
                
    function Foo(){ 
        getName=function(){console.log(1);}; 
        return this;
    }
    function getName(){console.log(5);}//函数声明优先提升
    var getName;//变量提升,但重复声明,被忽略
    Foo.getName=function(){console.log(2);};//为Foo函数创建了一个静态属性存储一个匿名函数
    getName=function(){console.log(4);};//赋值操作留在原地,此处赋给变量getName一个匿名函数
    Foo.getName();//第一问访问Foo函数的静态属性getName并立即执行该函数,输出是2
    getName();
    Foo();
    getName();
      第一问就如注释里所说的,是对Foo函数的静态属性成员函数的调用,输出是2。下面分析第二问,第二问执行getName()函数,引擎会去寻找函数名称标识符getName是否有被声明过,此时发现该函数已经声明过了,大家可能就想当然的以为输出会是5。然而,你可能会忽略一个细节那就是在执行第二问之时,它前面的语句已经按从上到下的顺序一条一条执行过了,也就是说,getName这个函数虽然最开始被声明过了,但是在后来又重新被赋值操作赋予了新的函数,最终应该执行该新的getName函数,所以第二问输出是4。这里还有一个奇怪的现象,如果你将这段代码当作一个整体来调试,也就是作为一个js文件放到网页里调试输出是4;而很多人喜欢在控制台里进行调试,而且是一句话一句话单独输入调试,这时你就会发现当你输入第三问按下回车的那一刻,控制台返给你的却是5。这就奇了怪了,难道控制台出错?浏览器有问题?可换了浏览器还是一样。个人对这个怪现象的解释是这样的,问题就出在你是单步执行每句代码的,控制台对于你的代码是输入一句按下回车就执行一句,而不是把整段代码当作整体一起先编译再执行。没有整体这个概念,也就没有声明提升的问题。那就回到原题没有经过提升的写法(只摘取相关主要代码):
               
     var getName=function(){console.log(4);};
     function getName(){console.log(5);}
     getName();//第二问
    当你输入完第一句按下回车,控制台就立即执行了第一句给getName赋了个匿名函数;此时你又键入了第二句按下回车,控制台又立马执行了第二句,因为第二句被执行了,getName函数的内容就又被刷新了;此时你再输入第三句执行getName函数自然是最新的输出5这个值。
     
      搞清楚了前两问我们再来看后两问,看起来第四问和第二问长的真是一毛一样,为何结果输出又不一样了呢,这就要问第三问干了些什么了。第三问执行了Foo函数,我们来看Foo函数里面长着什么样子:
               
     function Foo(){ 
         getName=function(){console.log(1);}; 
         return this;
     }
    这个Foo函数竟然可以操纵getName并给getName赋了一个匿名函数,真是不简单啊!我们来分析一下他为什么可以这样做,正常情况下,执行Foo函数里的第一句话时,引擎会去寻找getName是否有被声明过,首先是在由Foo函数所产生的函数作用域里,咦,发现没有找到,于是根据作用域嵌套规则他锲而不舍地向上级作用域(即全局作用域)寻找,发现了原来getName小弟在这儿。引擎高兴了,就执行了Foo函数的第一句把匿名函数赋给了getName,getName函数就又被刷新了。getName小弟可真是命途多舛啊,谁让你不找个其它作用域大哥罩着你呢,因为引擎只会沿着嵌套的作用域一直向上寻找标识符(即内部作用域可以访问外部变量),而外部作用域是没有办法访问内部作用域内的变量的。看,一句Foo();竟干了这么多大事,直接导致了第四问执行getName()函数的结果输出是1。真是令人折服啊!以上,就是鄙人对这道题的理解。毕竟自己的实战经验还是太少,如果理解有误欢迎大家批评指正!非常感谢!同时感谢原题作者King老师的分享,以及我所参考的图书《你不知道的JavaScript》的作者KYLE SIMPSON和译者赵望野、梁杰,通过阅读此书让我对作用域有了一定的认识,再次感谢!
     
    所引用的原题原文链接:http://www.imooc.com/article/13893
  • 相关阅读:
    C++ 指针 new delete int*与string
    61.Android适配的那些P事(转)
    60.Android通用流行框架大全
    Android Studio配置指南总结
    大数据学习资源(下)
    大数据学习资源(上)
    59.Android开源项目及库 (转)
    Linux 简介
    7款应用最广泛的Linux桌面环境盘点
    58. Android一些开发习惯总结
  • 原文地址:https://www.cnblogs.com/sammiyu/p/my_tech2_hoisting.html
Copyright © 2011-2022 走看看