关于提升和作用域的一道有趣的题目

  前几天看到了一道慕课网上推荐的有趣的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
posted @ 2016-10-29 13:54  乐鱼  阅读(677)  评论(0编辑  收藏  举报