JavaScript的历史包袱还是挺多的,之前写过一篇博客,讲述了如何从AO和GO的角度去理解JavaScript预编译、闭包,但是大牛winter说这样理解方式是ES3的,已经不能解释现在很多语法了。不过要去理解这些预编译的历史问题,我觉得此角度再合适不过。这也可能是我最后一篇为ES3、ES5的重难点知识写博客了。
直接上代码:思考以下打印结果
function f(a) {
console.log(a);
var a = 2;
console.log(a);
function a() {}
console.log(a);
}
f(1);
这道题目考察的是预编译的详细过程:
- 创建AO对象AO{ }
- 找形参和变量声明;将变量和形参名作为AO的属性名,值为undefined
- 将实参值与形参统一
- 在函数体里面找函数声明,值赋予函数体
预编译后,函数f的AO应该是这样的:
AO : {
a:function a() {}
}
预编译之后就是开始执行,值得注意的是,console的打印顺序与执行顺序(包括函数在哪里调用)有关;而预编译与函数在哪里定义有关,与函数在哪里调用无关。
打印结果:
function a() {} //第一个console
2 //执行了a=2之后
2
如果对那个形参传值1有疑问的,建议你好好理解一下上述预编译过程。
事情还没完,我们对这道题进行变形:
function f(a) {
console.log(a);
var a = 2;
console.log(a);
(function a() {
console.log(a); //这是第一个a函数
})();
function a() {
console.log('1');//这是第二个a函数
}
console.log(a);
}
f(1);
是不是懵了,我在里面加了立即执行函数表达式,而且其中不是匿名函数,而是具名函数,更恶心的是,名字又是a,在里面还要打印a。《你不知道的JS》里面提到,虽然使用具名函数的IIFE并不常见,但它具有上述匿名函数表达式的所有优势,因此也是一个值得推荐的实践。
winter对此编译环节是这样解释的:“具有名称的函数表达式”会在外层词法环境和它自己执行产生的词法环境之间产生一个词法环境,再把自己的名称和值当做变量塞进去。
也就是说预编译后的结果是这样的:
重点看立即执行函数的编译结果,他会在中间创建一个词法环境,把自己的函数名放进去。
代码开始执行:
- 第一个console打印,在f的AO下查找,找到了a,打印出来为
function a() {console.log('1');}
- 代码执行到第二个console,由于执行过a=2,所以f的AO中的a被赋值为2,打印2
- 代码执行到立即执行函数,即函数在当前位置定义并执行了,定义的过程预编译已经做完了,所以直接执行函数体内的东西,在“第一个a函数的AO”内查找,找不到,向上查找,再IIFE词法环境中找到了,于是打印
function a() {console.log(a);}
- 与2查找过程相同,打印2
看到这里或许有些疑问,如果把具名函数换成匿名函数,那IIFE词法环境就没有那个函数名变量了是吗?
没错!
function f(a) {
console.log(a);
var a = 2;
console.log(a);
(function () {
console.log(a); //被换成匿名函数了
})();
function a() {
console.log('1');
}
console.log(a);
}
f(1);
编译结果:
第三个console一直向上查找,一直找到f的AO中的a,因为执行过a=2,所以打印2
/*打印结果*/
function a() {console.log('1');}
2
2
2