闭包:
前置条件: 词法作用域的规则——内部函数总是可以访问其外部函数中声明的变量
触发条件: 当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,
结果: 我们就把这些变量的集合称为闭包。
文字有点拗口,我们看代码,有下面一段代码
<script type="text/javascript">
function foo() {
var myName = "zhangsan"
let num1 = 1
const num2 = 2
var innerBar = {
getName:function(){
console.log('num1:', num1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName("lisi")
bar.getName()
console.log(bar.getName())
</script>
JS执行方式为 先编译再执行,我们先来看看编译阶段与执行阶段分别发生了什么
一: 先编译全局代码
- JS调用栈中有
全局执行上下文
变量环境
foo = function () {...}
bar = undefined
outer = null
- JS可执行代码有
全局可执行代码
bar = foo()
bar.setName("lisi")
bar.getName()
console.log(bar.getName())
二:开始执行代码
1.全局代码编译完成此时调用栈如下
2.执行全局可执行代码到 bar = foo()时, foo是函数,符合一段代码的条件,所以要先编译再执行
3. 编译foo函数完毕,此时调用栈如下
foo函数可执行代码有:
myName = "zhangsan"
num1 = 1
num2 = 2
innerBar = {
getName:function(){
console.log('num1:', num1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
4.执行foo函数可执行代码,当foo函数可执行代码全部执行完毕,JS调用栈如下
5.bar = foo()执行完毕。继续往下执行bar.setName("lisi")时,因为setName是函数,符合一段代码的条件。所以需要先编译,再执行。当编译完成时调用栈如下:
此时在JS调用栈中 foo的可执行上下文已经从栈顶弹出, 所以从外部是访问不到myName属性的。
但是根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量,所以就在JS调用栈中开辟了 一个foo函数的闭包,setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 num1,所以这两个变量依然保存在调用栈的内存中。
通过控制台的Sources的scope栏目可以清晰的看到作用域链的信息
6.setName函数编译完成之后,执行setNmae函数的可执行代码 myName = ‘lisi’, 根据词法作用域可以看出作用域链是 setName的作用域 —》 foo闭包 —》全局作用域。此时再setName的变量环境以及词法环境都没有找到myName变量,所以开始往foo闭包开始找,在foo闭包找到myName 并赋值
7.执行到 bar.getName()时,getName()符合一段代码的标准,需要先编译再执行。且此时setName函数已执行完毕,从调用栈的栈顶弹出,此时调用栈如下
8.getName函数编译完成,执行可执行代码 console.log('num1:', num1) return myName。首先打印num1的时候,再getName的变量环境和词法环境没有找到num1变量,开始往foo找到num1变量同时打印出来,接下来执行return myName 在foo闭包找到myName 并执行 return 'lisi'
9.getName函数执行完成之后,getName执行上下文弹出,开始执行 console.log(bar.getName()),其中getName符合一段代码的条件,先编译再执行,此时调用栈如下
10.getName函数编译完成,执行可执行代码 console.log('num1:', num1) return myName。在foo闭包里找到num1并打印出1,然后执行return 'lisi'。getName执行完毕,执行console.log('lisi')。
11.所以最终是 打印两个1,一个lisi
闭包回收
当函数执行完毕,该函数的执行上下文就会从调用栈的栈顶弹出,而达到回收的目的
如果引用闭包的函数是一个全局变量,那么闭包会一直存在,页面关闭才会被释放;但如果这个闭包以后不再使用的话,就会造成内存泄漏,所以如果使用次数少的话,尽量使它成为局部变量