到目前为止,同学你知道了JavaScript的历史,也了解其“你想是啥就是啥”的变量系统。相信凭借你深厚的Java或者C++功底,再加上程序员特有的自傲气质,你肯定会信心满满:自信写JavaScript毫无压力。我也相信写个Script对于后端攻城师们那肯定不在话下。但是,当结果匪夷所想的时候,你或许会一番吐槽:真TM见鬼了,会不会是什么bug?还是浏览器有问题?我的代码逻辑没问题啊......。就像如下代码,你能说出结果是什么吗?
var a=123; var b=999; function func(a){ var b; console.log(a);//?????? 结果是什么????留着分析 var a=888; c=1111;function a(){ } console.log(a);//?????? 结果是什么????留着分析 console.log(b);//?????? 结果是什么????留着分析 console.log(c);//?????? 结果是什么????留着分析 } func(456);
是的,你的代码没有问题,当然浏览器也没问题。你或许说我才不会写得满屏都是“a”的代码!讲真的,当你看到这段代码的时候,你有没有想过为什么JavaScript能够这样重复定义同名的变量?本楼敢打赌十个看客中,能有一个提出这个疑问,那已经是惊喜了。可能有人会说“因为它是弱类型言语”,这个答案只能说对了一半。这看似很不科学、很不严谨的变量定义,怎么能够运行起来呢?很明显不科学。答案是:有人动了你的代码!
有人动了你的代码!有人动了你的代码!有人动了你的代码!重要的事说三遍!那是谁动了你的代码呢?故事又开始了。
这事还得回到九十年代JavaScript出生那会。话说布兰登-艾奇当时创造JavaScript的时候,他的需求就是做做客户端的数据验证而已。于是乎,他想“这玩儿没必要搞高能设计,看上去好像也没有什么地方需要高能运算的,搞预编译、链接器那是太浪费了,再说这玩儿是在浏览器上跑的,搞编译器、链接器,那浏览器不成了IDE啦?最好能像Perl那样,边解析边运行最美不过”。鞋同们看到这里应该明白了:那么多废话,你就不是为了说JavaScript是边解析边运行的嘛!我懂的,这个课本上有说。但是好多课本好像只说了边解析边运行,但是没说是怎么解析的,就算有说了,那也是废话比这篇博文还多,还说不清楚。到此,前面高呼三声那个问题的答案,想必看官到此也看出答案了:解析器动了你的代码!
解析器动了你的代码!那得先认认真真说下“从你敲下代码,然后运行,最后输出结果”这个过程到底发生了什么?课本都说了“边解析、边运行”,毫无疑问这个过程就分为“解析期”与“运行期”。那下面我们就以上面的代码为例,看看你的代码是怎么被动了手脚后再运行的。
解析期
先照本宣科说说楼主对解析期的理解:解析期就是每一个运行单元在代码运行前,解析器对用户代码(程序员写的代码)进行解析调整的时期。这里有个关键的术语“运行单元”。什么是运行单元?这里仅以浏览器环境做说明(nodejs环境可能不一样)。简单地理解,一个页面是一个运行单元,一个function也是一个运行单元。一个页面的JavaScript在运行前,页面的所有JavaScript声明定义都被解析调整一遍;在一个function在运行前,这个function内的所有JavaScript声明定义(包括形参)都被解析调整了一遍。看了本楼的个人见解(如有误,请斧正),你或许会问:按你的意思页面加载完成的时候,先解析了一次页面上的JavaScript,之后在调用function的时候又进行了一次解析,那岂不是有n次解析?对!没错,有n次解析!鞋同你看准了,楼主特意高亮的【JavaScript声明定义】。那什么是声明定义呢?且看代码:
var a;//是声明定义 var a=123;//包含了声明定义、赋值运算表达式 function f(){//是一个function定义 } var f=function(){//包含了声明定义、function赋值运算表达式 }
看官要是有耐心看到这里,你应该明白了什么是解析期,也了解了什么是JavaScript声明定义。本楼再次强调“解析器只对声明定义”进行解析调整,像上面的“var a=123”、“var f=function(){}”会被拆为两部分,声明定义及赋值运算!声明定义用于解析期,赋值运行用于运行期。那解析器是怎么解析调整JavaScript的声明定义的呢?下面以博文第一段给出疑问的示列代码func函数做分布分析。
第一步:JavaScript运行时,发现准备要调用func(456)
第二步:func是一个函数执行单元,在执行前,需要解析调整
第三步:为func执行单元准备一个当前的ActivityObject活动对象,即在func执行单元内生成一个所谓的活动对象,伪代码为:var AO={};
第四步:先解析func形参定义,发现func定义了一个形参a,那么将a挂到AO对象上,并且将实参赋给形参,AO={a:456}
第五步:解析变量声明定义,发现定义了var b,AO={a:456,b:undefined}
第六步:解析变量声明定义var a=888,拆分为var a;a=888;发现AO中已经有了a定义,不做调整,AO={a:456,b:undefined}
第七步:解析函数定义,发现function a(){}函数定义,AO={a:function(){},b:undefined}
怎么样!看官,知道解析器是怎么动了你的代码吧。你写的所有声明定义都被移动到了一个活动对象上!请记住,解析器是这样动你的代码的:准备活动对象,然后解析形参而且进行实参赋值,然后解析函数内的var 变量声明定义(如果包含赋值则拆分赋值运算)、然后再解析函数定义。
到目前为止,解析器偷梁换柱的工作做完了,一切就绪,只欠Running!那Running什么?剩下的那些代码就是Running的,如var a=888、c=111、console.log()。就是运行期里面要发生的事情。那接下来,说说运行期的事情,结果便会分晓!
运行期
运行期,那就是直接跑代码咯,没什么定义好说的。但是这个运行期还有个令人惊讶的地方。这家伙每遇到一个变量(包括函数变量),都会先从当前的ActivityObject中查找是否存在,如果不存在则往上查找(作用链?原型链?这里预留下一篇博文)。这个奇怪的行为就造成了前面博文提到的神奇的变量提升作用。看官,你终于知道什么是变量提升了吧,也知道变量提升是什么鬼造成的了吧!好!废话少说,咱们还是规矩分析下运行期是怎么跑代码的。
第一步:运行console.log(a),找AO对象,发现a=function,所以第一个结果是function(){}
第二步:运行var a=888,找AO对象,发现有个a定义,执行赋值运算,此时AO={a:888,b:undefined},函数被覆盖了!
第三步:运行c=1111,找AO对象,没货!往上找,还是没货,好吧,到处没货,那只能留给父亲大人了,于是c变成了父亲大人的成员,并赋值为1111
第四步:运行console.log(a),找AO对象,发现有料,a=888,结果是888
第五步:运行console.log(b),找AO对象,发现有料,b=undefined,结果undefined,特别声明:undefined和xxx is not defined是两回事!
第六步:运行console.log(c),找AO对象,没货,找父亲大人的,发现父亲大人有个c=1111,结果是1111
各位看官,时间不早了,看看写得也差不多了。看完这篇博客,你应该知道了咱们写的代码是被动过后,再运行的。