阅读 335

详细解析 JavaScript 闭包

有的老师说JavaScript两大神兽:原型和闭包

有的老师说JavaScript两大难点:异步和闭包

反正不管哪一个都有闭包,可见真的是非常重要了。

今天我就详细说一下闭包,看完你一定会有收获的。如果没看明白,一定是你没好好看。如果还没看明白,可以评论私信我给你讲。反正我自认为两大神兽已经攻克,我就是无敌的小萝莉。 。:.゚ヽ(。◕‿◕。)ノ゚.:。+゚

我看了两个老师的课,第一个老师讲的很深入,但是条理不太清晰,所以到最后我也感觉没听懂。直到第二个老师上来就仔细剖析了概念,我才恍然大悟。今天就结合起来写一下。

@[toc]

 


概念

什么是闭包?要想了解闭包什么是闭包,先得从概念抓起来。

 

闭包(closure)是指有权访问另一个函数作用域中变量的函数       ——《JavaScript高级程序设计》

(!!!记住这个英文单词,下边会有用)

 

简单理解就是: 一个作用域可以访问另一个函数内部的局部变量。

 

下图中三块作用域嵌套在一起:全局作用域中有一个Fun的函数作用域,Fun中有个局部变量a,还有一个函数foofoo也有自己的函数作用域。不管是从foo中调用Funa变量,还是从全局作用域中调用Funa,都会形成闭包。

 

在这里插入图片描述


如何产生闭包?

  • 当内部函数引用了外部函数的变量(函数)时, 就产生了闭包

  • 当子函数引用了父函数的变量(函数)时, 就产生了闭包

 

上面两种说法表达的是一个意思,拿概念中的图来说就是foo调用了Fun的变量或函数。

 

解决问题1

  • 当内部函数引用了外部函数的变量(函数)时, 就产生了闭包

  • 当子函数引用了父函数的变量(函数)时, 就产生了闭包

 

你可能会说:“我听不懂。怎么内部引用外部变量就产生闭包了啊”

 

下边就举几个例子说明一下。

举个栗子:chestnut: 不调用内部函数时已经产生闭包

                     function Fun() {

                            var a = 1

                            function foo(){

                                   console.log(a)

                            }

                     }

                     Fun()

复制代码

在上边这段代码中,我们看一下控制台,我把断点打在Fun()这一句上。这个时候可以看到右边scope被我圈出来了,这时候下边只有一个global。

然后点击执行下一步。

 

如果不知道为什么打在这里,或者说我下边提到的匿看不懂,那你就需要回去学习变量提升、作用域、执行上下文的内容。

在这里插入图片描述

这个时候就到了var a = 1这一步上。右边的scope已经发生变化,出现一个local。local里边还有一个foo。看看上边红框框里我圈出来的调用栈,现在已经在调用Fun这个函数了。也就是说local现在指的是Fun这块作用域。

在这里插入图片描述

这时候你点开==local=>foo=>[[Scopes]]=>0==你就发现了新大陆,里边有closure,里边写的a: undefined也就是上边我让你记的闭包的英文单词。而这个Closure(Fun)意思就是Fun的闭包已经形成了。在变量提升的帮助下,只要内部函数被解析,内部函数不需要执行,就已经产生了闭包。

在这里插入图片描述

为什么Fun的闭包已经形成了?

看下图。因为这个时候foo已经被解析了,也就是说它已经调用a了,所以形成闭包了。但是a=1这句还没执行,所以a现在是undefined。

在这里插入图片描述

举个反例:chestnut:加深理解不调用内部函数时已经产生闭包

如果你代码写成下边这样他就不会形成闭包。

在这里插入图片描述

查看一下控制台,确实没有闭包

在这里插入图片描述

知道为什么吧,再提醒一次,不知道就回去补习变量提升、作用域、执行上下文

因为只有function声明的函数才会被提升。var声明的函数只会提升函数名。也就是说这时候函数体还没有解析,foo函数还没有调用到Fun的a变量。不信你可以继续往下自己试一试,当进入foo函数只会就会出现闭包的嗷(´,,•ω•,,`)

在这里插入图片描述

举个例子:chestnut:内部函数调用产生闭包

看完上边两个例子,你当然那已经懂了,函数解析时候已经产生闭包了,调用的时候当然有闭包。 只不过区别是调用的时候a已经被赋值了。所以现在可以看到a:1了。

在这里插入图片描述

再举个例子:chestnut:内部函数执行时不调用外部函数的变量,不产生闭包

下图可以看出来,内部函数和外部函数的变量没有关系哦。所以内部函数即使执行了,右边scope那也没有显示产生闭包哦。(●′ω`●)

在这里插入图片描述

解决问题2

  • 当内部函数引用了外部函数的变量(函数)时, 就产生了闭包

  • 当子函数引用了父函数的变量(函数)时, 就产生了闭包

 

你可能会问:“你给出的概念那不是说调用变量吗,现在怎么加上(函数)了,
调用其他函数作用域中的函数也会形成闭包?”

 

举个例子:chestnut:内部函数调用外部函数的其他子函数产生闭包

不管是普通变量还是函数,只要内部的函数调用了外部函数的东西,就是闭包!!!

在这里插入图片描述

解决问题3

  • 当内部函数引用了外部函数的变量(函数)时, 就产生了闭包

  • 当子函数引用了父函数的变量(函数)时, 就产生了闭包

 

你可能还问:“你概念那不是说外部作用域调用Fun的变量也会产生闭包吗?我没
看到外部怎么调用啊。并且你这两条怎么都说的内部函数,没提全局作用域啊”

 

澄清一下,外部作用域的确可以调用某个函数的内部变量或函数,但也是借助该函数内部嵌套的子函数的。

举个例子:chestnut:外部作用域如何调用内部变量产生闭包

                     function Fun() {

                            var a = 1

                            function foo() {

                                   console.log(a)

                            }

                            return foo;

                     }

                     var aa=Fun();

复制代码

看一下上边这段代码,我帮你理一下。

上面这个代码运行的时候,先进行全局上下文的处理,进行变量提升。然后执行aa=Fun()赋值语句,调用Fun函数。此时进入Fun,Fun进行函数中的执行上下文处理。此时代码逻辑上会变成下图的样子:

在这里插入图片描述

 

进入Fun函数的时候,变量提升的时候就已经产生了闭包了。因为下图中断点打在a=1上。此时这句还没执行,也就是说还没给a复制,右边已经有闭包了。并且显示a:undefined

在这里插入图片描述

点击执行下一句,就会到return foo,此时a已经赋值了,右边闭包可以看到a:1

在这里插入图片描述

到这里我们可以知道,在全局中给aa赋值时候,Fun中就产生闭包了,全局作用于就是Fun的玩不作用域,现在就完成了外部作用域访问函数内部变量而产生闭包了吧。

综上所述产生闭包的条件

  • 函数嵌套

  • 内部函数引用了外部函数的数据(变量/函数)


闭包的生命周期

产生

产生:在嵌套内部函数定义执行完时就产生了(不是在调用)

经过上面的几个例子我们已经可以知道闭包是什么时候产生的了。

内部函数定义执行完时有下边几层含义

  • 不需要执行内部函数就可以产生

  • 内部函数解析的时候产生

 

上边的第一个、第二个、最后一个例子都能看出来,function声明的函数在变量提升的时候解析了,那时候就已经产生了。如果不使用function声明的,用var声明的,那么在给var变量赋值函数的时候,函数解析也会产生闭包。不需要执行内部函数就可以产生

死亡

死亡:在嵌套的内部函数成为垃圾对象时

什么叫内部函数成为垃圾对象时

  • 调用内部函数的变量被销毁时。

 

借用上边最后一个例子继续来看。上边我已经提到了,执行var aa=Fun();的时候已经产生闭包了。那么现在把断点打在下边的aa()上看一下。

在这里插入图片描述

执行aa()就相当于调用了foo函数,点击下一步查看aa()执行的情况。这时候发现右边会显示存在闭包,因为你调用aa,aa引用了Fun的变量。也就是说aa执行的时候闭包是存在的。第一个aa执行完之后闭包依然存在,因为还有第二个aa,虽然我下面没写了,但是你怎么知道实际写程序的时候下边不会继续用到呢。

在这里插入图片描述

 

所以只要你赋值之后不管执行不执行,闭包是一直都存在的。如果想让他消失,就要销毁调用内部函数的变量,也就是使aa=null,执行之后闭包就消失了。

在这里插入图片描述


闭包的作用

  • 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

这个在解决问题3上已经说明了。忘了的上去看。

  • 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)

这个在闭包的生命周期 死亡上边刚解释了。忘了的上去看。

 

问题

  1. 函数执行完后, 函数内部声明的局部变量是否还存在?

       一般来说是不存在了。但是使用闭包的情况下可以使它继续存在(实例见闭包的生命周期

  2. 在函数外部能直接访问函数内部的局部变量吗?

       一般来说不能,但是可以通过闭包来操作,使内部的变量对外部可见。

 

 ***

常见的闭包

  • 将函数作为另一个函数的返回值

在这里插入图片描述

  • 将函数作为实参传递给另一个函数调用

在这里插入图片描述


闭包的应用——定义JS模块

JS模块是什么

  * 具有特定功能的js文件

  * 将所有的数据和功能都封装在一个函数内部(私有的)

  * 只向外暴露一个或n个方法的对象或函数

  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能

 

两种写法

使用外部函数返回值

下图中将内部的两个函数作为返回值,外部声明变量之后即可使用。

在这里插入图片描述

使用匿名函数自调用

使用匿名函数自调用向外暴露方法,就可以直接调用不用声明变量了。

在这里插入图片描述


闭包的缺点和解决方法

###  缺点

  * 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长

  * 容易造成内存泄露

 

比如下图中,我创建了一个超大数组。现在外部aa赋值之后产生闭包了。如果我忘了释放,那就会导致占用内存时间边长,并且泄露内存。

在这里插入图片描述

解决

  * 能不用闭包就不用

  * 及时释放

       就是var aa = Fun()使用完之后及时aa=null

 

补充

内存溢出:

  • 一种程序运行出现的错误

  • 当程序运行时候需要的内存超过了剩余内存,就抛出内存溢出的错误。

 

内存泄漏:

  • 占用的内存没有及实释放

-  内存泄漏积累多了就会导致内存溢出

  • 常见的内存泄漏

       - 闭包

       - 意外的全局变量

       - 没有及时清理的计时器或回调函数

 


搞几个例题?

例题1: 输出什么?


                     var name = "The Window";

                     var object = {

                            name: "My Object",

                            getNameFunc: function() {

                                   return function() {

                                          return this.name;

                                   };

                                  

                            }

                     };

                     console.log(object.getNameFunc()());

复制代码

在这里插入图片描述

因为你是直接调用getNameFunc的,所以getNameFunc的this指针是指向全局的,输出The Window

 

例题2:输出什么?


                     var name2 = "The Window";

                     var object2 = {

                            name2: "My Object",

                            getNameFunc: function() {

                                   var that = this;

                                   return function() {

                                          return that.name2;

                                   };

                            }

                     };

                     console.log(object2.getNameFunc()());

复制代码

在这里插入图片描述

你是直接调用getNameFunc的,所以getNameFunc的this指针是指向全局的。

但是在定义的时候,有个var that = this,定义的时候函数是作为object2的一个方法,所以那时候this指向object2。用that存储了那个时候的this指向。即使之后在全局中调用,this的指向改变了,也不会影响that,所以输出My Object

 

例题3:输出什么? (建议不要看,我感觉我解释了你可能也看不懂。只会徒增纠结。当然如果看了不懂可以留言。)


                     function fun(n, o) {

                            console.log(o)

                            return {

                                   fun: function(m) {

                                          return fun(m, n)

                                   }

                            }

                     }

                     var a = fun(0)

                     a.fun(1)

                     a.fun(2)

                     a.fun(3)

                     var b = fun(0).fun(1).fun(2).fun(3)

                     var c = fun(0).fun(1)

                     c.fun(2)

                     c.fun(3)

复制代码

在这里插入图片描述

在这里插入图片描述

看一下执行a.fun(1) 的时候,调用栈显示两个fun,也就是确实又回到了function fun(n,x)。这个fun是由于最里边的返回值调用的,所以这时候又产生了新的闭包。然后a.fun(1) 又传入一个1,这时候原来的闭包还存在,那就相当于传入两个参数fun(1,0)。所以输出0。执行完毕后,由于全局中并没有一个变量存储a.fun(1) ,因此执行完后刚才新产生的那个闭包就死亡了。但是变量a存储的fun(0)这个闭包还是存在的。因此执行a.fun(2) a.fun(3)的时候同理,新产生的闭包传入新的参数,和原来闭包传入的参数一起。所以都会输出0

在这里插入图片描述

变量b是生成四层闭包并且保存了下来。

在这里插入图片描述

在这里插入图片描述

到变量c这里就好理解多了,是c存储了两层闭包。剩下的c.fun(2),c.fun(3)生成的闭包都不会保存下来,因此执行的时候都是在两层闭包的基础上执行的。

在这里插入图片描述

所以之后执行的都是会将1变为x,仅在执行的时候传入n的新值2或3。


 

╭(●`∀´●)╯怎么样看懂了吧!我可真是小机智。

我是萝莉安,还没当程序媛的小机智。

文章分类
前端
文章标签