你不知道的JavaScript:闭包

前言

在了解闭包的概念时,我但愿你可以有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()做用域
固然,这儿还有另一个例子:code

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

本例中,baz()在foo()以外调用,而且baz()自身有着涵盖foo()做用域的引用,所以baz()能够记住foo()的做用域,保证其不会被垃圾回收机制销毁ip

如今我懂了

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

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')

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

循环与闭包

给一个经典的案例io

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,这里的解决方案有不少:console

  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中,充满神奇色彩的一部分,可是当咱们揭开她的面纱,才发现她居然这么美,她一直陪在你身边,可是你却一直逃避她,此次我不想你再错过她了。

相关文章
相关标签/搜索