0. 前言
JS Closure
闭包,平时我们可能感觉用到的不多,但是在面试中是非常常见的问题之一,一般用来检验面试者对于JS
基础的掌握程度,很多同学可能看过但是印象不深,也有可能感觉自己弄懂了,但是问的一些问题或者题目又答不上来。这篇文章,我们就好好讲一讲JS Closure
,把闭包
讲通。
1. 定义
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
解读一下官方定义,可以看出闭包
是一种代码结构或者说是能力(也可以说是特定的代码结构产生的能力),这种结构或者能力可以让开发者访问一些常理来说不能访问的东西。
首先先有一个概念,下面我们再来展开的讲一讲。
2. 词法环境和作用域
官方定义中涉及到一个名词:Lexical Envrionmnet 词法环境
,展开说的话要从JS
编译解析的原理开始讲起,内容太多了(开个坑以后有机会可以展开讲讲)。
简单来讲,JS
编辑过程中有一个过程会一行一行的去处理你的代码,处理的时候会将标识符
(变量和函数的名字)和引用的映射记录在一个小本上,下次用到的时候就从这个小本子的记录里面去找。有一点特殊的地方这个记录的结构是一层一层的嵌套关系,内层记录有外层的引用。
每个本子上能访问到的记录,这个范围就是作用域
。
3. 深入探索
来看下面的例子:
function outter() {
let name = "test";
function inner() {
console.log(name);
}
inner();
}
outter();
// 输出:test
为什么能够正常输出test
?就是因为闭包
,闭包
是使用者可以通过内部方法来访问外部变量。内部inner
方法没有name
这个变量,但是因为JS
的词法环境是会有外层的引用,所以会读取到outter
中的name
。
打印出内部方法可以看到,内部有属性保存着外部的作用域的引用。
我们先暂停一下上一个例子的思考。先大概的讲一下运行的原理。JS
在运行过程中执行函数的时候,会有一个调用栈。当方法执行时,就会放到栈中执行,执行完毕就出栈。(为什么是栈?栈这种数据结构先进后出,保证嵌套的执行顺序,以后我们机会可以讲一下DFS和BFS,会对栈和队列有更清晰的理解)
于是我们再看上面的例子,自然有这个画面:outter
推到执行栈中执行,附带了他的一些变量(作用域中的引用,执行的时候要用),执行内部,有inner
调用,于是将inner
推到执行栈中,此时outter
还没释放,inner
执行的时候,读name
,能够找到是应为outter
在执行栈中,所以变量跟着outter
一起的?也在执行栈中?
真的是这样吗?我们来看下下面的例子:
function outter() {
let name = 'test';
function inner() {
console.log(name);
}
return inner;
}
let f = outter();
f();
// 输入:test
按照我们刚才的理解,推演一遍:outter
执行,进栈,执行完成后返回inner
方法赋值给f
,outter
出栈释放变量。f
执行进栈执行,打印name
,但是这个时候outter
已经不在执行栈中了,还是能找到name
,哦吼?因为数据是存在堆(Heap)
中,执行完成也不会释放。
我们再回到最开始说的,闭包
是一种能力,访问常理不能访问的东西。按常理name
跟随outter
释放确实不应该能访问到。但是因为闭包
,使得我们可以打印出name
。
4. 应用场景
由于这种即使外层方法结束了,但是通过调用内部方法仍然可以访问到外层作用域的变量。我们可以实现一些操作。下面的示例出资官方文档:
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
为什么会不一样?因为add5
和add10
保存的是不同的词法环境,所以会返回不同的结果。于是我们很方便的就可以实现一个工厂模式
。
当然还有很多其他应用,以后可以开个专题来展开讲讲。(又挖一坑)
5. 隐含问题
由于使用闭包之后,在外层调用结束后,仍会保存属性,不会释放,所以可能会导致内存占用过高,内存泄漏的问题。我们在使用时要考虑是否真的需要用到闭包
,谨防滥用。
6. 总结
以上我们介绍了JS Closure
,一般来说,闭包
出现总是有函数的嵌套和内层函数引用外层变量。不知道我有没有讲清楚,也欢迎大家在评论区讨论。