你不知道的JavaScript:闭包

wolaoreme 2019-06-27

前言

在了解闭包的概念时,我希望你能够有JavaScript词法作用域的知识,因为它会让你更容易读懂这篇文章。

感触

对于那些使用过JavaScript但却完全不理解闭包概念的人来说,理解闭包可以看做是某种意义上的重生,但是你需要付出大量的努力和牺牲才能理解这个概念。
回忆我一年前,虽然使用过很多JavaScript,但却完全不理解闭包是什么。当我了解到模块模式的时候,我才激动地发现了原来这就是闭包?

JavaScript中闭包无处不在,你只需要能够识别并拥抱它。

开始

直接上定义
当函数可以记住并访问所在的作用域时,就产生了闭包。即函数是在当前词法作用域之外执行

function foo () {
    const a = 2
    function bar () {
        console.log(a)
    }
    return bar
}
const baz = foo()
baz() // 2  ---  妈妈呀!这就是闭包?太简单了吧!

函数foo()使用它的内部方法 bar()作为返回值,而bar()内部有着对foo()作用域的引用(即a),在执行foo()过后,内部函数bar()赋值给baz,调用baz()显然可以执行bar()。
可以看到bar()在自身作用域之外执行了,通常在foo()执行过后,我们会觉得foo()会被JS引擎的垃圾回收机制销毁,实际上并不会,因为baz有着对bar()的引用,而bar()内部有着foo()作用域的引用,因此foo()并不会被销毁,以供bar()在任何时间被引用,因此bar()记住了并访问了自身所在的foo()作用域
当然,这儿还有另外一个例子:

function foo () {
    const a = 2
    function baz () {
        console.log(a)
    }
    bar(baz)
}
function bar (fn) {
    fn() // 这就是闭包
}

本例中,baz()在foo()之外调用,并且baz()自身有着涵盖foo()作用域的引用,因此baz()可以记住foo()的作用域,保证其不会被垃圾回收机制销毁

现在我懂了

上一节的代码过于死板,我们来看看更实用的代码。

function wait (message) {
    setTimeout(function timer () {
        console.log(message)
    }, 1000)
}
wait('hello')

很明显,内部函数timer()持有对wait()的闭包
或者在jQuery中

function setupBot (name, selector) {
    $(selector).click(function activator () {
        console.log(name)
    })
}
setupBot ('hello', '#bot')

可以看到,闭包在你写的代码中无处不在,特别是回调函数,全是闭包

循环与闭包

给一个经典的案例

for(var i = 1 ; i <= 5 i ++) {
    setTimeout(function timer () {
        console.log(i)
    }, i * 1000)
}

你可能会天真的以为它会输出:1,2,3,4,5?
事实上,它会以每秒一次的频率输出5次6
为什么?
因为延迟函数会在循环结束时才执行。就算你setTimeout(...,0),也会在循环完成时,输出5次6
当然,不要以为主要的原因是延迟函数会在循环结束时才执行,不然我为什么会在闭包这一节用使用这个例子,哈哈。
那么真正导致这个与预期不符的是闭包
首先内部函数timer()有着涵盖for循环的闭包,这5次调用timer()都是封闭在同一个作用域中,他们共享同一个i,只有一个i
那么我们如何让它按照我们的预期,输出1,2,3,4,5呢?
当然是让每个timer(),都有一个属于自己的i,这里的解决方案有很多:

  1. IIFE立即执行函数可以形成一个块作用域,我们只需要把每次迭代的i,保存在timer()的块作用域中,通过这个保存的值打印出来就ok了

    for(var i = 1 ; i <= 5; i ++) {
          (function() {
            var j = i
            setTimeout(function timer () {
              console.log(j)
            }, i * 1000)
          }
          )(i)
      }
  2. ES6中的const或者let,它们都可以构造一个块级作用域(PS:const 定义常量,无法被修改

    for(var i = 1 ; i <= 5; i ++) {
        const j = i
        setTimeout(function timer () {
            console.log(j)
        }, j * 1000)
    }
  3. 我们可以用let稍微改进一下(为什么在for循环中使用let,不用const,上面已经说得很清楚了

    for(let i = 1 ; i <= 5; i ++) {
        setTimeout(function timer () {
            console.log(i)
        }, i * 1000)
    }

不知道你怎么想,反正块级作用域闭包的使用,让我成为了一只快乐的JavaScript程序员

模块

这是闭包运用得最广的地方了吧
看看下面的代码

function Module(){
    const something = 'Do A'
    const another = 'Do B'
    function doA(){
      console.log(something)
    }

    function doB(){
      console.log(another)
    }
    return {
      doA,
      doB
    }
  }
  const foo = Module()
  foo.doA()
  foo.doB()

这种模式,在JavaScript中被称为模块,其中包含的闭包,相信大家一眼就看出来了吧。
Module()中的 doA() 与 doB() 都包含了对Module()的闭包
那么模块模式需要具备的条件是:

  • 必须有外部的封闭函数,且至少被调用一次(每次调用都会产生一个新的模块)
  • 封闭函数必须返回至少一个内部函数,形成闭包,并且可以修改和访问私有状态。

由于调用一次就会产生一个模块,那么是否有单例模式呢?

const foo = (function Module(another){
    const something = 'Do A'
    function doA(){
      console.log(something)
    }

    function doB(another){
      console.log(another)
    }
    return {
      doA,
      doB
    }
  })()
  foo.doA()
  foo.doB('Do B')

通过IIFE,立即调用这个模块,只暴露foo,那么这个模块只有foo这一个实例。

现在的模块机制

// bar.js
function hello(who) {
    return `hello ${who}`
}
export hello
// foo.js
// 仅导入hello()
import hello from 'bar'

const name = 'jack'
function awesome () {
    console.log(hello(name))
}
export awesome
// baz.js
// 导入完整模块
module foo from 'foo'
module bar from 'bar'

console.log(bar.hello('john'))
foo.awesome()

这里模块文件中的内容同样被当做好像包含在作用域中的闭包一样处理

小结

闭包就好像是JavaScript中,充满神奇色彩的一部分,但是当我们揭开她的面纱,才发现她竟然这么美,她一直陪在你身边,但是你却一直逃避她,这次我不想你再错过她了。

相关推荐