这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
前言
闭包问题不好答,原因往往不在于闭包本身有多么晦涩神秘,而在于闭包的背后有太多太多的故事可以深挖。面试 经历稍微丰富一些的同学会发现,大多数面试场景下,面试官不会直接问你“闭包是什么”,而是会直接甩出来一套代码片段给你,问“这段代码的运行结果是什么?”。遇到这种情况,大家真要在心里松一口气了,毕竟直接脑内跑代码。是闭包最温和、痛苦程度最小的一种考察方式。针对这种考察方式,我们本节也会有习题给到大家来做。但 除此之外,我希望大家能对另一种提问方式引起注意 ——“你如何理解 JavaScript 中的闭包” ?
当面试官问你“如何理解 ”的时候,他大概率并不是想听你背诵“闭包是一种 xxxxxx 的函数”,而是想跟你聊聊作用域、作用域链等触及 JS 语言核心的一些知识点,聪明的面试官,还会借机引出变量提升、暂时性死区、执行上下文等附加话题,甚至想问问你 JS中的不同异常之间本质的区别在哪里?词法作用域模型又是啥?
作用域的实现机制
TIP 👉 大家知道,几乎每一种编程语言,它最基本的能力都是能够存储变量当中的值、并且允许我们对这个变量的值进行访问和修改。那么有了变量之后,应该把它放在那里、程序如何找到它们?这是不是需要我们提前约定好一套存 储变量、访问变量的规则?这套规则,就是我们常说的作用域。更多时候,我们提到作用域的时候,指的是这个规则约束下的一个变量、函数、标识符可以被访问的区域(这时它就更具体了)。
我们来看一个简单的声明语句:
var name = '青莲'
JS 会怎么理解这句 “话” 呢?
在JS引擎眼里,它包含了两个声明:
var name (编译时处理)
name = ‘青莲’ (运行时处理)
何为编译时、何为运行时?难道 JS 不是不存在编译阶段的 “动态语言” 吗?
事实上,JS 也是有编译阶段的,它和传统语言的区别在于,JS 不会早早地把编译工作做完,而是一边编译一边执行。简单来说,所有的 JS 代码片段在执行之前都会被编译,只是这个编译的过程非常短暂(可能就只有几微妙、或者更短的时间),紧接着这段代码就会被执行。
回到我们这个语句上来,我们来看看编译阶段和执行阶段阶段都发生了什么事情:
编译阶段: 这时登场的是一个叫 编译器 ****的家伙。编译器会找遍当前作用域,看看是不是已经有一个叫 name 的家伙了。如果有,那么就忽略 var name 这个声明,继续编译下去;如果没有,则在当前作用域里新增一个name。然后,编译器会为引擎生成运行时所需要的代码,程序就进入了执行阶段
执行阶段: 这时登场的就是大家常常听到的 JS ****引擎 ****了。JS 引擎在执行代码的时候,仍然会找遍当前作用
域,看看是不是有一个叫 name 的家伙。如果能找到,那么万事大吉,我来给你赋值。如果找不到,它也不会灰心,它会从当前作用域里 “探出头去”,看看 “外面” 有没有,或者 “外面的外面” 有没有。如果最终仍然找不到 name 变量,引擎就会抛出一个异常。
这里出现了一个有趣的东西,就是我们引擎的查找过程 —— 何谓探出头去?何谓 “外面” 呢?这就引出了我们 JS作用域里一个非常重要的概念 —— 作用域链。
作用域套作用域,就有了作用域链
现在我们已经知道,作用域本质上就是程序存储和访问变量的规则。上个小节,我们聊过了作用域在 JS 这门语言中的实现机制。现在,我们来看看,这套规则的内容具体是怎么回事儿。
在 JS 世界中,目前已经有了三种作用域:
全局作用域
函数作用域
块作用域
作用域链
在我们实际开发中,通常不止用到一种作用 域。当一个块或者一个函数嵌套在另一个块或者函数中时,就发生了作用域的嵌套。比如这样:
function addA(a) {
console.log(a + b)
console.log(c) // 报错
}
var b = 1
addA(2) //3
它们的关系示意如下:
我们试图在 addA 这个函数里访问变量 b 的时候,考虑到函数作用域内并没有对 b、c 这两个变量作定义,所以一开始肯定是找不到的。要想找到 b、c ,该怎么做?就是我们上文提到的“探出头去”,对吧?探出头去,去上层作用域(全局作用域找),找到了 b ,那么就可以直接拿来用了;没找到 c,并且全局作用域已经没有上层作用域了(头探不出去了),那就歇菜,报错!这就是上文“执行阶段 ”里我们描述的那个过程。 在这个查找过程中,层层递进的作用域,就形成了一条作用域链。上面这个例子里,作用域链比较短:
理解闭包
function addABC(){
var a = 1,b = 2;
function add(){
return a+b+c;
}
return add;
}
var c = 3
var globalAdd = addABC()
console.log(globalAdd()) // 6
在这个例子里,作用域嵌套的情况展示如下:
作用域链关系展示如下:
其中 add 这个函数,它嵌套在函数 addABC 的内部,想要查找 a、b、c 三个变量,它得去上层的 addABC 作用域里找,对吧?像 a、b、c 这样在函数中被使用,但它既不是函数参数、也不是函数的局部变量,而是一个不属于当前作用域的变量,此时它相对于当前作用域来说,就是一个自由变量。而像 add 这样引用了自由变量的函数,就叫闭包。