代码已经关联到github: 链接地址 文章有更新也会优先在这,觉得不错可以顺手点个star,这里会持续分享自己的开发经验(:
定义
一个函数和对其周围状态(Lexical Environment,词法环境,详细可看《执行上下文、作用域到底是什么?二者有什么关系》) 的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说
举个例子:
function bar(){
let a = 1
return function foo(){
let b = 1
return a + b
}
}
bar()()//2
在上述的例子中,其作用域伪代码如下:
GlobalEnvironment = {
EnvironmentRecord: {
// 标识符
},
outer: null
};
barEnvironment = {
EnvironmentRecord: {
a: 1,
bar: '<fun>'
}
outer: GlobalEnvironment
};
fooEnvironment = {
EnvironmentRecord: {
b: 1,
}
outer: barEnvironment
};
因为静态作用域的特点,foo
函数会从bar
中获取a
的值,但是这里的bar
已经执行完毕,执行上下文已经出栈了,a
变量肯定也不存在了,这就与我们闭包的定义矛盾了。所以这里闭包函数对外层函数的变量必定会特殊处理,以保证闭包函数可以使用,我们可以认为闭包就是将引用的外部函数的变量中记住了,类似于引用变量,存储在了“堆”中。
用途
创建私有变量
闭包让函数以外也可以访问到函数内部的变量,这样可以用来创建私有变量,比如:
//简单闭包
function demo(){
var a = 1
return function(){
return a
}
}
var t = demo()
t()//1
常见的用法就是防抖和节流函数:
// 防抖
function debounce(fn,wait){
let timer
let that
let arg
const delayFn = function(){
const _that = that
const _arg = arg
that = arg = undefined
timer = null
return fn.apply(_that,_arg)
}
return function(){
that = this
arg = arguments
timer = setTimeout(()=>{
if(timer){
clearTimeout(timer);
}
timer = setTimeout(delayFn, wait);
})
}
}
// 节流
function throttle(fn,wait){
let timer
let that
let arg
const delayFn = function(){
const _that = that
const _arg = arg
that = arg = undefined
timer = null
return fn.apply(_that,_arg)
}
return function(){
that = this
args = arguments
if(!timer){
timer = setTimeout(delayFn, wait);
}
}
}
模块化
通过匿名立即执行函数,单独开辟一个作用域,使得只暴露相关接口函数,达到模块化。
//仅调用一次的设计模式——单体模式
var module = (function(global){
var text = '1'
var fn1 = function (){
// ...
console.log(text,1)
}
var fn2 = function fn2(){
// ...
console.log(text,2)
}
return {
fn1: fn1,
fn2: fn2
}
//也可以直接用暴露到window上
//global.myModule={
// fn1: fn1,
// fn2: fn2
// }
})(window)
module.fn1()//1 1
module.text //undefined
// myModule 暴露到window上
解决for循环中定时器函数取值异常问题
下文的代码中,我们期望在延迟函数中输出的是1,2,3,4,5
,但实际上却并非如此:
//异常情况
for(var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);//6 6 6 6 6 6
});
}
这是由于setTimeout
内的函数发现自身i
这个变量未定义时,会作用域链去查找i
,而这里的i
是var
声明的,只会声明一次,也就是说后续i++
都会修改这个值,最终i
变成了6
,等到setTimeout
函数执行时,输出的自然都是6
。
我们可以采用闭包的方式,单独开辟一个作用域给每次循环的setTimeout
,这样输出就会跟我们所想的一致了。
//正常情况
for(var i = 1; i <= 5; i++) {
(function(j){
//这里是块级作用域 var j = i
setTimeout(function timer() {
console.log(j);//1 2 3 4 5 6
});
})(i);
}
缺点
- 错误的使用可能导致内存泄漏。闭包函数对象会一直引用它被创建时的作用域对象,由于这个引用一直存在,所以不会被浏览器的垃圾回收机制给回收。
- 变量可能发生改变
//变量可能发生改变
function test() {
let i = 1
var result = function(){
console.info(i) //2
}
i = 2
return result
}