一、深入了解闭包,从作用域链开始
- 什么是作用域链?
- 每一个执行上下文的变量环境中都存在一个
outer指针,用来指向外部的执行上下文(指向词法环境)。当 V8 引擎在查找一个变量时,在当前执行上下文中没有找到,就会顺着outer所指向的执行上下文继续查找,以此类推,直到找到全局为止。我们把这个查找的链条关系称为作用域链。
先看一段代码
let count = 1 // 全局作用域
function main(){
let count = 2 // main作用域
function bar(){
let count = 3 // bar作用域
foo() // 调用外部定义的foo
}
bar()
}
function foo(){
console.log(count); // 输出: 1 (访问全局count)
}
main()
- 这段代码执行时先在栈中创建一个全局执行上下文,全局变量与函数声明有
count变量,main函数和foo函数,注意let与const定义的变量是在词法环境中,outer指针为null(全局已经是最外层) - 然后全局调用了
main函数,再创建一个main 函数执行上下文,全局变量与函数声明有count变量,bar函数,outer指针指向全局词法环境。 main函数内部又调用了bar函数,此时再创建一个bar 函数执行上下文,全局变量与函数声明有count变量,outer指针指向main 函数词法环境(定义在main函数里)。bar函数内部又调用了foo函数,最后又创建一个foo 函数执行上下文,outer指针指向全局函数词法环境(定义在全局)。foo函数执行console.log(count),此时会先在当前执行上下文中找count变量,由于没有找到,会顺着outer找,由于outer指向全局,全局count值为 1,因此最终输出结果为 1。
- 具体流程如下图
二、什么是闭包
- 一个函数执行完毕后,它的执行上下文会被销毁
- 根据作用域的查找规则,内部函数一定可以访问外部函数中的变量
- 当一个外部函数中的内部函数被拿到外部函数之外来执行,哪怕外部函数执行完毕了,被内部函数引用的那部分变量依然需要被保留。我们把这部分变量的集合称之为闭包。
代码示例
function add() {
let count = 0
return function() {
count++
return count
}
}
var bar = add()
console.log(bar()) // 1
console.log(bar()) // 2
console.log(bar()) // 3
- 这段代码执行时先在栈中创建一个全局执行上下文,全局变量与函数声明有
bar变量、add函数,outer指针为null。 - 然后全局执行
add()函数,创建一个add 函数执行上下文,词法环境中存放let count = 0,outer指针指向全局词法环境;add函数执行完成后,返回内部匿名函数,并赋值给全局变量bar,add执行上下文被销毁,但count变量因被内部的匿名函数引用不会被销毁,而是留下一个空间存放count变量 - 全局第一次调用
bar()(匿名函数),创建匿名函数执行上下文,自身无变量声明,outer指针指向add 函数词法环境;执行count++,找到add中的count并自增为1,返回1,执行完毕上下文出栈销毁。 - 全局第二次调用
bar()(匿名函数),再次创建匿名函数执行上下文,outer指针依旧指向add 函数词法环境;执行count++,找到add中的count,自增为2,返回2,执行完毕上下文出栈销毁。 - 全局第三次调用
bar()(匿名函数),执行count++,count值自增为3,返回3。
- 匿名函数执行
count++时,会先在当前执行上下文中找count变量,由于没有找到,会顺着outer找,由于outer指向add函数词法环境,找到持久保存的count变量,因此依次输出结果为1、2、3。具体流程如下图(第2,3次创建匿名函数执行上下文省略)
三、闭包的应用:解决循环问题
使用 var 定义循环变量时,由于其没有块级作用域的特性,会遇到经典的问题:
var arr = []
for (var i = 1; i <= 5; i++) {
arr.push(function() {
console.log(i)
})
}
for (var n = 0; n < arr.length; n++) {
arr[n]() // 输出 6,6,6,6,6
}
- 这里通过在循环内部创建一个函数
fn,将当前的i作为参数j传入。由于形成了闭包,每个内部函数都保留了对各自j值的引用,从而解决了var循环变量的问题,正确输出1,2,3,4,5
for(var i = 1; i <= 5; i++) {
function fn(j){
arr.push(function(){
console.log(j);
})
}
fn(i)
}
优点:
- 定义私有模块,防止全局变量污染
- 允许函数记住其创建时的环境
缺点:
- 可能导致内存泄漏,因为被闭包引用的变量不会自动释放