一. 前言
在第一篇文章之中,我们知道了作用域链的查找规则即内层作用域一定可以访问到外层作用域中的变量对象的;在第二篇文章之中,我们知道当一个函数执行完成之后,它的执行上下文是一定会被销毁出栈的。那么现在请你思考一个问题,当函数的执行上下文被销毁时,我们还能够访问到它作用域中的变量吗?乍一看,你可能会觉得这个问题好奇怪,函数的执行上下文已经被销毁出栈了,我们还怎么拿到它内部的变量的呢。嘿嘿嘿,其实是可以的,今天我们就来聊一下其中的一种方法------闭包。
二. 辨析作用域链和执行上下文
在此之前,我们先来复习一下之前的内容。在之前的文章中,我们说到作用域链 相当一个由当前作用域和上层所有作用域所组成的链表,用于查找变量的值。作用域链的查找规则是由内向外的。执行上下文是代码执行时的运行环境,JS 引擎每执行一段代码就会创建一个执行上下文并压入执行栈。当代码执行完毕时,执行上下文会被销毁出栈
现在让我来忽悠一下你,来看下面这段代码
JS
var a = 10;
function foo2() {
console.log(a);
}
function foo1() {
var a = 20;
console.log(a);
foo2();
}
foo1();

按照上一篇文章来分析的话,程序开始执行,首先会创建全局执行上下文,压入栈底,随着代码运行创建
foo1
的执行上下文并压入栈,最后创建foo2
的执行上下文压入栈,如上图所示。那当执行到函数 foo2时,要console.log(a)
,那么这个a
是不是也是应该按照由内向外的原则往 foo1里面找呢?嘿嘿嘿,然后让我们来运行一下

嗯?怎么回事,难道不是输出
20 20
吗?你可能已经发现问题了,这是执行上下文呀,我们的作用域可不是这样的。被我忽悠到了吧,它的作用域链结构大概是这样的

现在你知道为什么输出为:
20;10
了吗
注意!!!
当我们在分析函数内部变量的取值时,千万不要将作用域链与执行上下文混淆了,他们两个是有区别的
- 执行上下文 是动态的,它是在代码运行时确定的,随时可能改变
- 作用域 是静态的,作用域中的值一旦确定,永远都不会改变。函数可以不被调用,但是作用域中的值,在函数创建时就已经被写入了,并且存储在作用域链对象里面
- 函数的作用域 是由它被定义的位置决定的,而不是在调用时决定的
三. 闭包
OK,讲清楚了作用域链和执行上下文,现在我们可以来聊闭包了。那什么是闭包呢?上代码
JS
function foo() {
var a = 1;
return function bar() {
console.log(a);
}
}
var baz = foo();
baz(); // 1
输出为:1,我们来看一下它的执行上下文和作用域链
按照作用域来看,函数 bar 可以读取到函数 foo 的 a,所以输出为1没错
按照执行上下文,当函数 foo 将函数 bar 返回出去后,foo 就已经执行完了,它的执行上下文会被销毁出栈。那么当函数 baz 执行时,foo 不是已经不存在了吗,它怎么能拿到 foo 里面的值并输出的。那他们两个不是矛盾了吗?是的,他们就是矛盾了。那么 JS 为了解决这个问题,就发明了闭包。由此我们可以得到闭包的定义。
1. 闭包的定义
定义在函数 A 内部的函数 B ,当其使用了外层函数 A 作用域中的变量 并被拿到函数 A 外部执行时 ,为了保证程序的正常执行,在 A 函数执行完毕后,会将函数 B 所需要的变量留在一个集合并存入调用栈,这个集合就是闭包。它包含了函数 B 本身以及它所捕获的外层函数作用域中的变量。
所以上述图可以画成这样
故函数 baz 可以拿到 foo 中的变量,输出为:1
2. 闭包的作用
- 可以访问到外部函数的作用域
- 封装私有化变量
- 解决全局污染的问题
3. 闭包的缺点
- 会造成内存泄漏
四. 总结
到这里,闭包就讲完啦,你弄明白了吗?回到最开始的问题,除了闭包你还能说出哪些方法可以在函数外部拿到函数内部的变量吗?把你的答案打在评论区吧