从一道作用域题目聊聊作用域

1.题目:

1
2
3
4
5
6
7
8
var a = 10;                 // 步骤0
(function () {
console.log(a) // 步骤1
a = 5 // 步骤1-1
console.log(window.a) // 步骤2
var a = 20; // 步骤2-1
console.log(a) // 步骤3
})()

2.分析:

我们知道js的运行流程分为两个部分,预编译期和执行期;先是预编译期接着是执行期。而在预编译期会进行词法分析等,预编译期导致的一个和我们强关联的结果那就是-变量声明提升。同时在es6出来之后,需要明白使用let和const声明的变量不会存在变量提升。

所以在上面预编译期间存在两处变量提升,分别是window.a 和 local.a。因此执行步骤1的时候此时由于Local作用域里面已经有变量a了,所以输出undefined。接下来运行步骤1-1,同样由于在预编译期间所导致的local.a,因此不会继续向上层作用域查找变量a。步骤0自然是挂载到了全局作用域下面。执行步骤2-1的时候,相当于local.a = 20;到这里基本就解释完毕了。

总结:var声明的变量在预编译期间会经历声明提升;如果在当前作用域没有找到某个变量,会向上层作用域进行查找,直到找到全局作用域,若是此时还没有找到的话,那么剩下的就是reference error了。

3.拓展,立执行函数的作用域分析:

目前断言,立执行函数的作用域就是window。问题为什么在jQuery的立执行函数中需要传入window呢?答案:使得在jQuery代码块中访问window时,不需要将作用域链回退到顶层作用域,实现更快的访问window。

所以可以看一下下面这个例子:

1
2
3
4
5
6
7
8
9
// 结果输出为 10 50 20
var a = 10;
(function() {
console.log(this.a);
a = 50;
console.log(a);
var a = 20;
console.log(a);
})();

4.拓展2:理解变量提升

1
2
3
4
5
6
// 输出1
fun();

function fun() {
console.log(1);
}

从上面可以看出,对于函数表达式来说,从创建,赋值都被提升了。

1
2
3
4
5
6
// 输出:报错TypeError: fun is not a function
fun();

var fun = function() {
console.log(1);
};

从上面可以看出,对于函数赋值语句来说,函数变量创建存在提升,但是赋值并不存在。

最奇怪的地方还是下面这种场景了:

1
2
3
4
5
6
// 输出 ReferenceError: a is not defined
var a = 20;
(function() {
console.log(a);
let a = 20;
})();

前面提到过let和const是不存在变量提升的,如果是这样的话,那么为什么不是输出20呢?(local作用域找不到a,继而向上层作用域寻找)所以说,let在预编译期间应该还是做了更多的动作。基于此,我的理解是:在预编译(parser)期间,首先在全局作用域产生了一个变量a,接着在local作用域发现由let所定义的a,创建它,但是是否提升呢?可以理解为提升了,但是同时又创建了一个暂时死区,提升的意思是告诉后面执行阶段的代码a是local里面的,暂时死区的意思是指 变量a从声明语句那一处位置才开始变得可访问,在这之前是暂时死区,在这暂时死区里面访问是会报错的。

总结:let和const存在提升,但是提升的意义是在于告诉所在的作用域你访问的就是我,不是别的哪个上层作用域;同时副作用是:创建了暂时死区,得等我被执行之后才可访问;否则报ReferenceError错误。