资讯专栏INFORMATION COLUMN

你不知道的JavaScript:闭包

weknow619 / 2379人阅读

摘要:回忆我一年前,虽然使用过很多,但却完全不理解闭包是什么。就算你,也会在循环完成时,输出次当然,不要以为主要的原因是延迟函数会在循环结束时才执行,不然我为什么会在闭包这一节用使用这个例子,哈哈。

前言

在了解闭包的概念时,我希望你能够有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,这里的解决方案有很多:

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

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

ES6中的const或者let,它们都可以构造一个块级作用域(PS:const 定义常量,无法被修改

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95120.html

相关文章

  • 重读你不知道JS (上) 第一节五章

    摘要:词法作用域的查找规则是闭包的一部分。因此的确同闭包息息相关,即使本身并不会真的使用闭包。而上面的创建一个闭包,本质上这是将一个块转换成一个可以被关闭的作用域。结合块级作用域与闭包模块这个模式在中被称为模块。 你不知道的JS(上卷)笔记 你不知道的 JavaScript JavaScript 既是一门充满吸引力、简单易用的语言,又是一门具有许多复杂微妙技术的语言,即使是经验丰富的 Jav...

    worldligang 评论0 收藏0
  • [JS]《你不知道Javascript·上》——词法作用域和闭包

    摘要:吐槽一下,闭包这个词的翻译真是有很大的误解性啊要说闭包,要先说下词法作用域。闭包两个作用通过闭包,在外部环境访问内部环境的变量。闭包使得函数可以继续访问定义时的词法作用域。 闭包是真的让人头晕啊,看了很久还是觉得很模糊。只能把目前自己的一些理解先写下来,这其中必定包含着一些错误,待日后有更深刻的理解时再作更改。 吐槽一下,闭包这个词的翻译真是有很大的误解性啊…… 要说闭包,要先说下词法...

    guqiu 评论0 收藏0
  • 你不知道javascript》笔记_作用域与闭包

    摘要:建筑的顶层代表全局作用域。实际的块级作用域远不止如此块级作用域函数作用域早期盛行的立即执行函数就是为了形成块级作用域,不污染全局。这便是闭包的特点吧经典面试题下面的代码输出内容答案个如何处理能够输出闭包方式方式下一篇你不知道的笔记 下一篇:《你不知道的javascript》笔记_this 写在前面 这一系列的笔记是在《javascript高级程序设计》读书笔记系列的升华版本,旨在将零碎...

    galaxy_robot 评论0 收藏0
  • 你不知道 JavaScript 笔记——作用域和闭包

    摘要:理解作用域在引擎看来是两个完全不同的声明。在循环中使用闭包闭包是函数和声明该函数的词法环境的组合。回到我们上面说的在自己定义的作用域以外的地方执行,这里声明的是全局变量,使用全局变量不构成闭包。 第一章:作用域是什么 程序中变量存储在哪里,需要是怎么找到它,这就需要设计一套存储以及能方便的找到它的规则,这个规则就是作用域 编译原理 JavaScript 是一门编译语言,它与传统编译语言...

    BearyChat 评论0 收藏0
  • 十分钟快速了解《你不知道 JavaScript》(上卷)

    摘要:最近刚刚看完了你不知道的上卷,对有了更进一步的了解。你不知道的上卷由两部分组成,第一部分是作用域和闭包,第二部分是和对象原型。附录词法这一章并没有说明机制,只是介绍了中的箭头函数引入的行为词法。第章混合对象类类理论类的机制类的继承混入。 最近刚刚看完了《你不知道的 JavaScript》上卷,对 JavaScript 有了更进一步的了解。 《你不知道的 JavaScript》上卷由两部...

    赵春朋 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<