JavaScript 闭包

对于JavaScript的学习来说,闭包这一块一直是玄而又玄的东西,初学者很难掌握,我是学了大半年的JS,现在又回过来看【闭包】这部分,这里稍微总结一下,我采用由浅入深一步步的方式讲解,为了能帮助初学者们更好的理解,我将从下面几个方面展开:

  • JavaScript在函数中定义其他函数的能力;
  • 传递函数对象的方式;
  • 在函数内部和外部定义的变量的作用域;
  • 由变量作用域和闭包导致的常见问题;
  • 在jQuery中使用函数;
  • 函数交互导致的内存问题。

1.创建内部函数

支持内部函数的语言允许我们在必要的地方集合小型实用函数,以避免【污染命名空间】。
所谓内部函数,就是定义在另一个函数中的函数


Paste_Image.png

innerFn()就是一个被包含在outerFn()作用域中的内部函数。这意味着在outerFn()内部调用innerFn()是有效的,而在outerFn()外部调用innerFn()则是无效的。


Paste_Image.png

这种技术特别适合于小型、单用途的函数。例如,【递归】通常最适合通过内部函数来表达

1.1在任何地方调用内部函数

JavaScript中的内部函数能够逃脱定义它们的外部函数。

  • 方法一,将内部函数指定给一个【全局变量】:


    Paste_Image.png

    在函数定义之后调用outerFn()会修改全局变量globalVar,此时它引用的是innerFn()。这意味着,后面调用globalVar()的操作就如同调用innerFn()一样,也会执行输出消息的语句。

  • 方法二,也可以通过在父函数中【返回值】来“营救出”内部函数的引用:


    Paste_Image.png

    这里从outerFn()中返回了一个对innerFn()的引用。通过调用outerFn()能够取得这个引用,而且,这个引用可以保存在变量中,也可以自己调用自己,从而触发消息输出。

JavaScript的机制是只要存在调用内部函数的可能,JavaScript就需要保留被引用的函数,只有最后一个变量废弃,它的【垃圾收集器】才能出面释放相应的内存空间。

1.2理解变量作用域

内部函数也可以拥有自己的变量,只不过这些变量都被限制在内部函数的作用域中:


Paste_Image.png

每当通过引用或其他方式调用这个内部函数时,都会创建一个新的innerVar变量,然后递增。

内部函数可以像其他函数一样引用全局变量:


Paste_Image.png

现在,每次调用内部函数都会持续地递增这个全局变量的值。

当这个变量是父函数的局部变量时,因为内部函数会继承父函数的作用域,所以内部函数也可以引用这个变量:


Paste_Image.png

通过每个引用调用innerFn()都会独立地递增outerVar。因为第二次函数调用的作用域中创建并绑定了一个新的outerFn()的【实例】。

当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的一个【闭包】。从本质上讲,如果内部函数引用了位于外部函数中的变量,相当于授权该变量能够被延迟使用。因此,当外部函数调用完成后,这些变量的内存不会被释放,因为闭包仍然需要使用它们。

2.处理闭包之间的交互

当存在多个内部函数时,很可能会出现意料之外的闭包。假设我们又定义了一个递增函数,这个函数中的增量为2:


Paste_Image.png

这里,我们通过【对象】返回两个内部函数的引用调用任何一个内部函数。

这两个内部函数引用了同一个局部变量,因此它们共享同一个【封闭环境】。当innerFn1()为outerVar递增1时,就为调用innerFn2()设置了outerVar的新的起点值,反之同理。

3.在jQuery中创建闭包

3.1 $(document).ready()的参数

由于我们通常把$(document).ready()放在代码结构的顶层,因而这个函数不会成为闭包。但是,我们的代码通常都是在这个函数内部编写的,所以这些代码都处于一个内部函数中:


Paste_Image.png

这里和前面的例子其实是一样的,只不过外部函数是传入到$(document).ready()中的一个回调函数。innerFn()引用了位于回调函数作用域中的readyVar,因此innerFn()及其环境就创建了一个闭包。两次调用这个内部函数,两次输出保持了readyVar的值。

3.2绑定事件处理程序

.ready()结构通常用于包装其他的jQuery代码,包括【事件处理程序】的赋值。因为处理程序是函数,它们也就变成了内部函数;而且因为这些内部函数会被保存并在以后调用,于是它们也会创建闭包:


Paste_Image.png

这里的变量counter可以被.click()处理程序中的代码引用,由于创建了闭包,每次单击按钮都会引用counter的同一个实例。也就是说,消息会持续显示一组递增的值,而不会每次都显示1。

事件处理程序同其他函数一样,也能够共享它们的封闭环境:


Paste_Image.png

因为这两个函数引用的是同一个变量counter,所以两个链接的递增和递减操作会影响同一个值,而不是各自独立的值。

3.3在循环中绑定处理程序

  • 从一个经典的错误说起:
    构造六个div,当点击一个div时,按照预期,应该打出相应div的值,但是它总是会显示div的数量。因为事件处理器函数绑定了变量i本身,而不是函数在构造时的变量i的值,i始终无法被释放。换句话说,即使在绑定处理程序i的值每次都不一样,每个click处理程序最终引用的i都相同,都等于单击事件实际发生时i的最终值(6)。
    Paste_Image.png

    解决这个问题的方式有很多。
  • 方法一:避免在循环中创建函数,它可能只会带来无谓的计算,还会引起混淆,正如上面那个经典的错误。我们可以先在循环外创建一个辅助函数,让这个辅助函数再返回一个绑定了当前i值的函数,这样就不会导致混淆了。改良后的例子,用正确的方式给一个数组中的节点设置事件处理程序


    Paste_Image.png
  • 方法二:我们用一个立即执行函数给它包住,我们不再依赖i,而是用另外一个变量n把它保留下来。


    Paste_Image.png

4、 应对内存泄漏的风险

4.1避免意外的引用循环

闭包可能会导致在不经意间创建引用循环。因为函数是必须在内存中的对象,所以位于函数封闭环境中的所有变量也需要保存在内存中:


Paste_Image.png

这里创建了一个名为outerVar的对象,该对象在内部函数innerFn()中被引用。然后,为outerVar创建了一个指向innerFn()的属性,之后返回了innerFn()。这样就在innerFn()上创建了一个引用outerVar的闭包,而outerVar又引用了innerFn()。


Paste_Image.png

这里我们修改了innerFn(),使它不在引用outerVar。但是,这样做仍然没有断开循环。即使innerFn()不再引用outerVar,outerVar也仍然位于innerFn()的【封闭环境】中。由于闭包的原因,位于outerFn()中的所以变量都隐含地被innerFn()所引用。因此,闭包会使意外的创建这些引用循环变得易如反掌。

4.2 控制DOM与JavaScript的循环

旧版本IE中存在一种难以处理的引用循环问题。当一个循环中同时包含DOM元素和常规的JavaScript对象时,IE无法释放任何一个对象,除非关闭浏览器。

Paste_Image.png

当指定单击事件处理程序时,就创建了一个在其封闭的环境中包含button变量的闭包。而且,现在的button也包含一个指向闭包(onclick属性自身)的引用。这样,就导致了在IE中即使离开当前页面也不会释放这个循环。
【解决方法】:

Paste_Image.png

因为hello()函数不再包含button,引用就成了单向的(从button到hello)、不存在的循环,所以就不会造成内存泄漏了。

【用jQuery化解引用循环】

Paste_Image.png

即便此时仍然会创建一个闭包,并且也会导致循环,但这里的代码却不会使IE发生内存泄漏。因为jQuery会手动释放自己指定的所有事件处理程序。只要坚持用jQuery的事件绑定方法,就无需为这种特定的常见原因导致的内存泄漏而担心。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 前言 总括 :这篇文章使用有效的javascript代码向程序员们解释了闭包,大牛和功能型程序员请自行忽略。 译者...
    KX九五阅读 270评论 0 1
  • 前言 总括 :这篇文章使用有效的javascript代码向程序员们解释了闭包,大牛和功能型程序员请自行忽略。 译者...
    秦至阅读 708评论 0 19
  • 前言 这篇文章使用有效的javascript代码向程序员们解释了闭包,大牛和功能型程序员请自行忽略。 基础篇 闭包...
    kiaizi阅读 345评论 0 7
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zouCode阅读 1,250评论 0 13
  • 闭包总是让人摸不着头脑。陆陆续续接触了一些闭包的知识,但还是不是非常明白,最近偶然看了一下 jQuery基础教程 ...
    深沉的简单阅读 150评论 0 0