深入理解JavaScript闭包
在开始讲闭包之前,我们需要理解作用域和作用域链
作用域链
什么是作用域链?
我们先看一段代码
function bar(){
console.log(myName)
}
function foo(){
var myName='崔斯特'
bar()
}
var myName='卡牌大师'
foo()
当我们看到这个题目的时候,我们会想到用执行上下文去分析,当执行到bar函数时,调用栈的状态如图:
文章图片
上图可以看到有两个myName变量,那bar执行的时候用的是哪一个呢?
其实在每个执行上下文的环境变量中,都包含了一个外部引用(称为outerouterouter),指向外部的执行上下文。
当在bar函数的执行上下文中没有找到myName变量的时候,会通过outer去外部的执行上下文中找这个变量。而bar的outer是直接指向全局执行上下文,然后在全局执行上下中,先在词法环境中从栈顶到栈底查找,如果没有再到变量环境中查找。
有人可能会问:为什么是foo函数调用bar函数,但是outer指向的是全局上下文?
- 这个其实和词法作用域(静态作用域)有关,简单来说就是代码结构中函数声明的位置来决定,上述的代码中,foo函数和bar函数的上一级作用域是全局作用域,所以如果foo或bar数调用了一个它们没有定义的变量,它们就会到上一级作用域中查找。说白了:词法作用域是代码阶段就决定好了,和函数是怎么调用没有关系
块级作用域的查找
文章图片
我们来说说上述的查找过程,当执行到bar函数的if语句时,因为bar函数的执行上下文中没有定义test变量,根据词法作用域规则,就会到bar函数的外部作用域中查找,也就是全局作用域。在单个执行上下文中的查找规则:先在词法环境中从栈顶到栈底查找,如果没有再到变量环境中查找。
闭包
了解完作用域链之后,我们从作用域链的角度来讲讲什么是闭包!
先看一段代码
function foo(){
var myName = "崔斯特"
let test1 = 1
const test2 = 2
var innerBar={
setName:function (newName){
myName = newName
},
getName:function (){
console.log(test1)
return myName
}}
return innerBar
}
var bar = foo()
bar.setName("卡牌大师")
bar.getName()
console.log(bar.getName())
根据词法作用域原则,innerBar中的两个方法可以访问foo函数的两个变量,当inner函数被返回给全局bar变量时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使?foo函数中的变量myName和test1。
看到这里我们可以给闭包下一个定义了!在JavaScript中,根据词法作?域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调??个外部函数返回?个内部函数后,即使该外部函数已经执?结束了,但是内部函数引?外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。?如外部函数是foo,那么这些变量的集合就称为foo函数的闭包。
闭包的回收 当我们闭包使用不正确时,很容易造成内存泄漏
- 如果引用闭包的函数是一个全局变量,那么这个闭包就会一直存在直到页面关闭,如果这个闭包不再使用的话,就会造成内存泄漏!
- 如果引用闭包的函数是一个局部变量,等函数销毁后,在下次js引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那垃圾回收器就会回收这块内容。
我们先来了解一下js在运行的过程中,数据是怎么存储的
在js的执行中有三种内存空间:代码空间、栈空间、堆空间
代码空间是存储可执行代码的,我们主要来看看栈空间和堆空间
栈空间和堆空间
前面讲的调用栈就是我们说的栈空间,存储执行上下文用的,我们来看一段代码:
function foo(){
var a = "崔斯特"
var b = a
var c = {myName:"崔斯特"}
var d = c
}
foo()
分析一下上面这段代码变量的存储,a和b是赋值着原始数据类型,所以他们会依次压入栈中的执行上下文的变量环境,但是c赋值的是引用类型,这时候的情况就不一样了。js引擎会把c、d分配到堆空间中,分配后会有一个堆地址,再把堆地址赋值给c。
可能现在你有疑问:把所有的数据存储在栈空间不好么?为什么要维护栈空间和堆空间呢?
a. 因为js引擎要用栈来维护函数的执行上下文,在一个函数执行结束后,当前函数的执行上下文栈区空间会被全部回收,然后js引擎要离开当前的执行上下文,只需要将指针下移到下一个执行上下文就可以了。如果栈空间太大的话,会影响执行上下文切换的效率,进而影响到整个程序的执行效率!
b. 通常情况下栈空间不会设置很大,主要是存放一些原始数据类型。堆空间比较大,适合存放一些占用空间较大的引用类型的数据。
内存模型视角的闭包
还是看上面的例子,foo函数的执?上下?销毁时,由于foo函数产?了闭包,所以变量myName和test1没有被销毁,?是保存在内存中。这个过程在内存中是怎么样的呢?
- js执行foo函数时,首先会编译,编译过程中遇到内部函数setName,js引擎还要对内部函数做一次词法扫描,发现内部函数引用了foo函数中的myName变量,js引擎会判断这是一个闭包,于是会在堆空间中创建一个"closure(foo)"对象(内部对象,js无法访问)来保存myName。
- 继续扫面,发现setName函数内部还引用了test1,引擎又将test1添加到closure(foo)对象中,这时候对象就包含了两个变量了。
文章图片
- 继续扫面,发现setName函数内部还引用了test1,引擎又将test1添加到closure(foo)对象中,这时候对象就包含了两个变量了。
推荐阅读
- 笨叔(用4维空间来理解进程负载)
- 操作系统进程切换的一些理解
- JavaScript中Infinity(无穷数)的使用和注意事项
- 深入理解nodejs的异步IO与事件模块机制
- JavaScript中集合引用类型就这么点东西
- JavaScript防抖与节流的实现与注意事项
- JavaScript深入理解节流与防抖
- JavaScript实现气球打字的小游戏
- Java并发——通过ReentrantReadWriteLock理解AQS的独占模式和共享模式
- 【面试普通人VS高手系列】谈谈你对AQS的理解