JS执行上下文详解(三):闭包

三、闭包

3.1 基础

了解了词法作用域链,接着我们就可以来聊聊闭包了。初学者对闭包的理解可能会是一道坎,刚接触时会产生一些挫败感,因为很难通过背后的原理来彻底理解闭包,从而导致学习过程中似乎总是似懂非懂。

当理解变量环境、词法环境和作用域链等概念后,再来理解JavaScript中的闭包会容易很多,下面看看这段代码:

<code>function foo() {    var myName = "IE";    let test1 = 1;    const test2 = 2;    var innerBar = {        getName:function(){            console.log(test1);            return myName;        },        setName:function(newName){            myName = newName;        }    }    return innerBar;}var bar = foo();bar.setName("Chrome");bar.getName();console.log(bar.getName());/<code>

熟练掌握JavaScript的同学一定能一眼看出答案,但是你真的清楚 ​getName​ 函数执行时的上下文吗?下面我们来详细分析一下这段代码执行时的上下文环境:

首先我们看看当执行到 foo 函数内部的 return innerBar 这行代码时的调用栈情况,如下图:


浏览器原理系列 - JS执行上下文详解(三):闭包


从上面的代码可以看出,innerBar 是一个对象,包含了 getName 和 setName 的两个方法(通常我们把对象内部的函数称为方法)。你可以看到,这两个方法都是在 foo 函数内部定义的,并且这两个方法内部都使用了 myName 和 test1 两个变量。

根据词法作用域的规则,内部函数getName 和 setName 总是可以访问它们的外部函数 foo 中的变量,所以当 innerBar对象被返回并赋值给全局变量bar时,虽然foo函数已经执行结束,但是getName 和setName函数总是可以使用foo 函数中的变量myName 和 test1。所以当foo函数执行完成后调用栈的状态如下:


浏览器原理系列 - JS执行上下文详解(三):闭包

从上图可以看出,foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。这像极了 setName 和 getName 方法背的一个专属背包,无论在哪里调用了 setName 和 getName 方法,它们都会背着这个 foo 函数的专属背包。

之所以是专属背包,是因为除了 setName 和 getName 函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为 foo 函数的闭包

看到这里,是时候搬出闭包的正式定义:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

下面我们接着分析,代码执行到 bar.setName 方法中的 myName = 'Chrome' 时,JavaScript 引擎会沿着“

当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量,你可以参考下面的调用栈状态图:

浏览器原理系列 - JS执行上下文详解(三):闭包

从图中可以看出,setName 的执行上下文中没有 myName 变量,foo 函数的闭包中包含了变量 myName,所以调用 setName 时,会修改 foo 闭包中的 myName 变量的值。

同样的流程,当调用 bar.getName 的时候,所访问的变量 myName 也是位于 foo 函数闭包中的。

我们可以通过Chrome的开发者工具来看看闭包的情况,在getName函数内部加上断点查看:


浏览器原理系列 - JS执行上下文详解(三):闭包

从图中可以看出来,当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况:Local 就是当前的 getName 函数的作用域,Closure(foo) 是指 foo 函数的闭包,最下面的 Global 就是指全局作用域,从“Local–>Closure(foo)–>Global”就是一个完整的作用域链。

3.2 闭包回收

通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。


分享到:


相關文章: