前言
关于javascript闭包其实已经是老生常谈了,我很久以前也写过有关闭包的文章,前段时间面试字节跳动的时候,面试官笑着和我说,闭包的内容可以再看看,以前我对闭包的认识和理解也是狭隘的,闭包是这门语言中极其难以掌握又非常重要的概念,这一篇文章的意义旨在帮大家真正理解javascript闭包
正文
javascript闭包无处不在
闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿来识别、拥抱和影响闭包的思维环境。为什么这么说,后面我会进一步举例说明
闭包的实质
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
很多文章把闭包解释为定义在函数里的函数,其实在一定意义上是不那么准确的,闭包真正的理解是,能给予在外部作用域访问它本不能访问到的作用域的能力,比如看一下下面的例子:
这是闭包吗?技术上来讲,也许是。但根据前面的定义,确切地说并不是。我认为最准确地用来解释bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分, 对于这个例子我们只要稍作调整就可以清晰地展示闭包
bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。**这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。**我觉得这个才是闭包的真正含义
闭包的真正意义
从上面的例子我们了解到,什么是闭包?函数在定义时的词法作用域以外的地方被调用。
因为javascript的作用域规则使用的是词法作用域,函数能访问的作用域永远保持它定义的位置,所以无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。这也实现了闭包真正的意义,通过这种机制,可以在外部作用域中访问不属于这个作用域的内容
循环和闭包
提到闭包,就不得不提一个典型的例子,那就是循环输出,看下面的例子:
会输出什么?很简单,5个6,为什么呢?因为延迟函数的回调会在循环结束时才执行。再往更深的原因分析,导致这个结果的原因是什么?是什么导致它不像我们想要的一样,1,2…6地来输出呢
因为我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
如果要解决这个问题应该怎么解决呢,我们真正需要的是一个局部的作用域,每个作用域里对应一个i,对应的问题就可以迎刃而解,很多同学可能第一反应就是利用let和const实现一个块作用域,除了此外还有什么办法呢?对了,还可以使用我们这章说的闭包来实现,利用闭包实现一个自执行函数,将i作为参数传进自执行函数,这样每个自执行函数都形成一块独立的作用域,里面有对应的i,就不会出现之前的情况
模块
我们之前提到,闭包可以实现封闭的作用域,并且可以让在这个作用域在别的位置使用,这个功能我们可以利用实现模块和API的封装。比如看下面的例子:
不过注意,在模块封装的过程中,需要在外部使用的内容需要用return暴露出来,我们也称这个为模块暴露,对于上面的模块模式,需要具备两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
如果只有一个,需要把上面的模式改装成单例模式,我们可以用自执行函数来调整,如下:
对于单例模式这些,不了解的同学可以看我之前写的有关设计模式的文章,这里就不再做赘述
小结
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。