「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」
前言
最近在看《你不知道的JavaScript》第一本,刚看完闭包那部分,感觉对JS的作用域、闭包机制有了全新的理解,在此分享一下我的学习感悟。希望以后被面试官问到这块时,能让他们觉得耳目一新。
浅谈JS编译过程
var a = 2;
JS的编译一般会经过三个过程,分别是词法分析、语法分析以及代码生成,以上面这行代码为例,当引擎执行代码生成的时候,编译器会进行如下处理:
- 当遇到
var a的的时候编译器会查找当前作用域中是否存在与a同名的变量,如果存在,则忽略这次声明,否则编译器就会在当前作用域中声明一个新的变量,并命名为a - 当遇到
a = 2的时候,引擎会查找当前作用域中是否存在一个名为a的变量,如果找到了,就会将它赋值给a这个变量,如果没找到,则会继续往外层查找(即外层作用域)
总结就是,变量的赋值操作会做了两个动作,先是编译器会在当前作用域中声明一个变量(如果此前没有声明过的话),随后引擎在执行代码的时候,会从当前作用域中查找这个变量,如果能够找到,就会给这个变量赋值。
LHS与RHS
其实在上述这个过程中,其实引擎进行了两次查询,分别位于赋值操作符的左侧与右侧,在编译原理中一般称为LHS(Left hand retrieve)与RHS(Right hand retrieve)。初次认识LHS与RHS,可能很难理解着两者的区别,我们可以简单的将它们理解为,赋值操作的目标(LHS)与赋值操作的源值(RHS)。文字比较晦涩,让我们来段代码看一下:
function foo(a) {
console.log(a); // 2
}
foo(2);
- 当引擎执行
foo(2)的时候,首先需要找到foo这个变量是啥,因此会进行一次RHS查询,并尝试从作用域中读取,如果成功,则开始执行foo函数 - 引擎遇到a变量,需要对
a进行一次LHS查询,为什么这里是LHS却不是RHS,因为此处我们需要给a这个形参分配值(隐式变量分配),我们需要找到a这个目标。 - 引擎遇到
console.log这个函数,需要进行一个RHS操作,因为需要使用到它的源值。 - 遇到内部的
a变量,也是一次RHS操作,这个值是2
作用域
在JS中,作用域负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。
当函数或者代码块有多层级嵌套的时候,就形成了作用域的嵌套。因此当引擎在当前作用域无法找到变量的时候,引擎就会往外层查找,直至抵达最外层的作用域为止。
在ES6以前,JS只有全局作用域和函数作用域,ES6之后JS出现了块级作用域。
闭包
闭包的概念实际上很简单,当一个函数能访问并记住当前作用域外的变量,使得这个变量无法被及时销毁时(垃圾回收),我们就称它为闭包。
LHS、RHS和作用域/闭包的联系
RHS
当JS引擎进行一次RHS查询的时候,引擎首先会在当前作用域中寻找变量,如果找不到,就会一直往外层找,这里也是作用域链的现实场景。如果RHS查询不到该变量,则会抛出一个ReferenceError,即引用错误,因为在当前执行环境中没有任何地方能找到该变量。
先来看一个作用域的例子:
let a = '';
// 赋值操作符的右侧会进行RHS查询,在lookup函数中无法找到b,并且到最顶层也找不到,所以引擎就会抛出一个ReferenceError
function lookup() {
a = b;
}
lookup();
再看看下一个与闭包的例子:
在函数内部,引擎会对a进行RHS查询,一路找到最顶层的作用域,而由于函数内部需要用到这个变量(打印),此时外部的a就无法被销毁,此时就形成了闭包
let a = '123';
function lookup() {
console.log('a => ', a); // '123'
}
LHS
当引擎进行一次LHS查询的时候,如果在作用域中找到了,则会将RHS时找到的值赋给赋值操作符左侧的值,在非严格模式下,如果引擎进行了一次失败的LHS查询,则会在作用域顶层创建一个与该值同名的变量,并将结果赋给这个新创建的变量上。
在我们的例子中,代码内从来没有对a进行过声明,所以此时的LHS操作其实是失败的
a = '123'; // 此时,引擎会对a进行一次LHS查询,如果失败了,会在作用域顶层创建一个同名变量(非严格模式)
console.log(window.a); // '123'
小结
正确理解LHS和RHS,对理解闭包、作用域的概念能起到拨云见日的效果,再次分享一下我的感悟,如果有写的不正确的地方,欢迎大家及时指正,互相交流