浅谈闭包

121 阅读4分钟

面试官视角的“闭包”

当面试官问你“如何理解”的时候,他大概率并不是想听你背诵“闭包是一种嵌套函数之类话语”,而是想跟你聊聊作用域、作用域链等触及 JS 语言核心的一些知识点,聪明的面试官,还会借机引出变量提升、暂时性死区、执行上下文等附加话题,甚至想问问你 JS 中的不同异常之间本质的区别在哪里?词法作用域模型又是啥?

理解作用域的实现机制

var name = 'bamboo';
  • 编译阶段(var name):一个叫 编译器 的家伙。编译器会找遍当前作用域,看看是不是已经有一个叫 name 的家伙了。如果有,那么就忽略 var name 这个声明,继续编译下去;如果没有,则在当前作用域里新增一个 name。然后,编译器会为引擎生成运行时所需要的代码,程序就进入了执行阶段;
  • 执行阶段(运行时处理,name = 'bamboo'):这时登场的就是大家常常听到的 JS 引擎 了。JS 引擎在执行代码的时候,仍然会找遍当前作用域,看看是不是有一个叫 name 的家伙。如果能找到,那么万事大吉,我来给你赋值。如果找不到,它也不会灰心,它会从当前作用域里 “探出头去”,看看 “外面” 有没有,或者 “外面的外面” 有没有。如果最终仍然找不到 name 变量,引擎就会抛出一个异常。

作用域链

作用域套作用域,就有了作用域链

  • 全局作用域
  • 函数作用域
  • 块作用域

LHS、RHS是什么?

LHS、RHS,是引擎在执行代码的时候,查询变量的两种方式。其中的 L、R,分别意味着 Left、Right。这个“左”和“右”,是相对于赋值操作来说的。当变量出现在赋值操作的左侧时,执行的就是 LHS 操作,右侧则执行 RHS 操作。

name = 'bamboo'

在这个例子里,name 变量出现在赋值操作的左侧,它就属于 LHS。LHS 意味着 变量赋值或写入内存 它强调的是一个写入的动作,所以 LHS 查询查的是这个变量的“家”(对应的内存空间)在哪。

var otherName = name

在这个例子里,第一行有赋值操作,但是 name 在操作的右侧,所以是 RHS;RHS 意味着 变量查找或从内存中读取,它强调的是读这个动作,查询的是变量的内容。

词法作用域和动态作用域

在 JavaScript 语言的范畴里讨论“作用域”这个概念的时候,确实不需要区分它是“词法”还是“动态”,因为我们 JS 的作用域遵循的就是词法作用域模型。当面试官抛出“词法作用域 ”这个概念的时候,完全不用慌,它指的就是你最最熟悉的 JS 作用域。

  • 词法作用域:也称为静态作用域,这是最普遍的一种作用域模型;
  • 动态作用域:相对“冷门”,但确实有一些语言采纳的是动态作用域,如:Bash 脚本、Perl 等。
var name = 'bamboo';
function showName() {
    console.log(name);
}
function changeName() {
    var name = '小竹子';
    showName();
}
changeName();

这是一段 JS 代码,不难答出它的运行结果是 'bamboo'。这是因为 JS 采取的就是词法(静态)作用域,这段代码运行过程中,经历了这样的变量定位流程:在 showName 函数的函数作用域内查找是否有局部变量 name;发现没找到,于是根据书写的位置,查找上层作用域(全局作用域),找到了 name 的值是 bamboo,所以结果会打印 'bamboo'。

那什么是动态作用域呢?动态作用域机制下,同样的一段代码,会发生下面的事情:

在 showName 函数的函数作用域内查找是否有局部变量 name;发现没找到,于是沿着函数调用栈、在调用了 showName 的地方继续找 name。这时大家看看它找到哪去了?是不是就找到 changeName 里去了? 刚好,changeName 里有一个 name,于是这个 name 就会被引用到 showName 里去,打印出'小竹子'。

能够修改词法作用域方式:eval 和 with