一、引言
首先看一个最简单的例子1
var a = 2;
开始,我们会以为这只是一个声明,但是事实并不是这样的,JS引擎认为这里有两个完全不同的声明,我们把这两个声明过程分解。
step1: var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a
step2: 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前作用域集合中是否存在一个叫做a的变量。如果是,引擎就会使用这个变量;如果不是,引擎会继续查找该变量;
如果引擎找到了变量a就将2赋值给它。否则引擎会抛出异常。
二、编译器的相关术语
在上述的例子中,我们用到了LHS查询,另外一种查询方式叫做RHS查询,接下来,我们说一说这两种查询方式的区别。
其实说到这里你大概能明白”L”和”R”代表的含义了,L代表Left,R代表Right;没错L和R代表一个赋值操作的左边和右边,即当变量出现在赋值操作的左侧时进行LHS查询,变量出现在赋值操作的右侧时进行RHS查询。再通俗一点说就是LHS表示找到一个变量对其进行赋值,而RHS表示的是一个赋值操作的源头
举个例子
1 | console.log(a) |
这里对a是一个RHS引用,因为a并没有被赋值,而我们只是想取得a的值,这样才能传递给console.log();
相比之下a = 2对a的引用则是属于LHS的,因为我们希望把=2这个操作加到a身上
引用一个书上的小测验,找出其中的LHS部分和RHS部分1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
/*
LHS部分:①在var c = foo(2)这一步,很明显的一个赋值语句,c=属于一个LHS
②在foo函数内部一个隐式的形参 a = 属于一个LHS
③在foo函数作用域内 var b = a;又一个明显的赋值语句,b=属于一个LHS
RHS部分:①在 var c = foo(2)处,我们需要知道foo(2)的值,再将其赋值给c,
因此= foo(2)是一个RHS
②在var b = a处,我们需要知道a的值是多少,因此= a是一个RHS
③④在return a + b处,我们分别需要知道a和b的值,因此这里有两个RHS
*/
三、关于LHS和RHS的异常
你也许会问我们为什么要关心两种不同编译方式的异常,,,Because这两种查询的行为是不一样的
1 | function foo(a) { |
b在RHS查询过程中是无法找到的,因为没有被声明,此时,在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常
相比之下,LHS查询过程会自动创建一个变量(非严格模式下),并将其返还给引擎
ps: 严格模式下会禁止隐式或自动创建全局变量,导致LHS查询同样会报ReferenceError异常
注:关于块级作用域和函数作用域以及变量提升的问题暂不做总结四、闭包
闭包是个老生常谈的问题,在我的面试过程中几乎都被问到了,for循环是我最初学习前端时最长踩的坑了,如下所示
1 | for (var i = 1; i <= 5; i++) { |
我们开始以为会以每秒一个的速度输出数字1-5,然而,现实是,每秒一次的频率输出5次6
首先说5个6是怎么来的,由于终止条件是i不再<=5,因此当i=6时第一次满足了这个条件,所以会输出i的最终值6;延迟函数的回调在循环结束时才执行,因此每次输出一个6。
Q:问题是到底为什么导致他的行为和语义所暗示的不一致?
A:我们试图假设循环中的每个迭代在运行时都会给自己”捕获”一个i的副本。但是由于作用域的工作原理,实际情况是尽管循环中的5个函数是在各个迭代中分别定义的,但是它们被封闭在一个共享的全局作用域中,因此实际上只有一个i,所有函数共享一个i的引用
我们默认你已经知道了立即执行函数的概念(IIFE),通过声明并立即执行来创建作用域1
2
3
4
5
6
7for (var i = 1; i<=5; i++){
(function() {
setTimeout(function timer(){
console.log(i);
}, i*1000);
})();
}
你觉得这样就行了???exo me ???连自己的变量都没有,还不是共用一个i,正确的打开方式是这样的1
2
3
4
5
6
7for (var i=1; i<=5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j*1000);
})(i);
}
关于块级作用域的一点注意:for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量