一、是什么
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。
即闭包让开发者可以从函数内部访问外部的作用域中定义的变量或者函数。
闭包=函数+外部作用域
词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
在 JavaScript
中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁
闭包形成的条件
- 函数的嵌套
- 内部函数引用外部函数的局部变量,延长外部函数的变量生命周期。
下面给出一个简单的例子
displayName()
没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变量
下面的例子更深入理解闭包是什么。
上图中画红框的位置我们能看到一个内部的对象 [[Scopes]]
,这个对象就是我们常说的作用域链。根据作用域链寻找的顺序,其中包含了闭包、全局对象。据此我们能通过闭包访问到本该销毁的变量,所以我会在上一章节中说原始数据一般存放在栈上。
闭包的特性
- 闭包让开发者可以从函数内部访问外部的作用域中定义的变量或者函数。
- 延长变量的生命周期在内存中维持一个变量。让这些外部变量始终保存在内存中。
二、使用场景
任何闭包的使用场景都离不开这两点:
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
调整字号的按钮
下面举个例子:
在页面上添加一些可以调整字号的按钮
柯里化函数
柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用
使用闭包模拟私有方法
闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
在JavaScript
中,没有支持声明私有变量,但我们可以使用闭包来模拟私有方法
下面举个例子:
上述通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式
两个计数器 Counter1
和 Counter2
是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量
用闭包模拟私有方法
高阶函数延迟调用
当我们调用 lazy_sum()
时,返回的并不是求和结果,而是求和函数:
调用函数f
时,才真正计算求和的结果:
其他
例如计数器、延迟调用、回调等闭包的应用,其核心思想还是创建私有变量和延长变量的生命周期
小结
闭包的两个场景,闭包的两大作用:保存/保护
。
- 前端 JavaScript 代码都是事件驱动的, 即一个事件绑定的回调方法;
- 发送 ajax 请求成功&失败的回调;
- setTimeout 的延时回调;
- 高阶函数延迟执行,一个函数内部返回另一个匿名函数。
- 用闭包模拟私有方法:将闭包作为公共方法,内部变量作为私有属性。
三、注意事项
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致 内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响
创建对象应关联对象的原型而不是定义在构造器中
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。
原因在于每个对象的创建,方法都会被重新赋值
上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
匿名函数创建了一个闭包,那么其访问的 i 是外部 test 函数中的 i,所以每一个节点实际上引用的是同一个 i。
每一个节点绑定了一个事件,这个事件接收一个参数,并且立即运行,传入 i,因为是按值传递的,所以每一次循环都会为当前 i 产生一个新的备份。
四、思考题
题目一
object.getNameFunc()这是属于方法调用,this指针指向的是object,可以用一个变量tmp引用它的结果,实际上tmp就是这个方法返回的那个匿名函数function(){return this.name;};
,此时并没有执行内部代码,执行tmp()时,也就是object.getNameFunc()()时,属于函数调用(另一篇博文详解了这里,链接),this指针指向window,最终返回The Window
。
题目二
在调用getNameFunc()时,属于方法调用,那么this指针指向object,把它被that引用,那么返回的匿名函数中时刻保持对object的引用,很好理解。
扩展阅读