JavaScript中闭包无处不在,你要做的就是识别并且拥抱它!
关于闭包,不同的人有不同的解释和认识,以下列举几个普遍的表达方式,以便让更多的人理解什么是闭包?
- 内部函数A可以引用外部函数B的参数和局部变量,当函数B返回函数A时,相关参数和变量都保存在返回的函数中,这种程序结构称为“闭包(Closure)”
- 当函数可以记住并访问所在词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行.
- 闭包是指有权访问另一个函数作用域中的变量的函数
接下来开始切入正题, 鉴于以上几个观点都分别涉及到了"作用域", 首先来介绍一下这个.作用域有两种工作模型.
- 词法作用域: 定义在词法阶段的作用域. 换句话说就是由你在写代码时将变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变(大多数情况如此)
- 动态作用域: 定义在代码运行时的作用域. 换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套.
主要区别: 词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行是确定的.(this也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用.
注意: JavaScript并不具有动态作用域,它只有词法作用域,但是this的机制某种程度上很像动态作用域.
你其实不需要有意的创建闭包,闭包其实在你的代码中随处可见,很多时候其实你就是没有识别闭包.
来看一段代码
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var ba = foo();
ba() // 2
ba执行时,拿到的是foo 的返回值 bar(),也就是说ba调用了内部函数bar(). bar()保持有foo内部作用域,对该作用域的引用,而这个引用就叫做闭包! 这个函数在定义时以外的地方被调用.闭包使得函数可以继续访问定义时的词法作用域.
再来看两段代码
function foo() {
var a = 2;
function baz() {
console.log(a) //2
}
bar(baz);
}
function bar(fn) {
fn()
}
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a)
}
fn = baz //将baz分配给全局变量fn
}
function bar() {
fn(); //实际就是调用baz() 这就是闭包
}
foo()
bar()
不论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包
无论通过何种手段将内部函数传递到所在词法作用域之外,它都会持有对原始定义作用域的引用,无论何时执行这个函数都会使用闭包
循环和闭包
for(var i = 1; i <= 5; i++){
setTimeout(()=> {
console.log(i)
}, i*1000)
}
上面代码大家期望的输出结果应该都是每隔一秒输出1~5,而实际是以每秒一次的频率输出5次6.
为什么会出现这样的问题呢?
因为我们以为循环中的每一个迭代都会"捕获"一个 "i"的副本.但是根据作用域的原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个 "i"
延迟函数的回调会在循环结束时才执行. 即使setTimeout(...,0),所有的回调函数依然是在循环结束后才会被执行.
那么怎么修改可以让它正常工作呢?
for(var i = 1; i <= 5; i++){
(function (){
var j = i;
setTimeout(()=> {
console.log(j)
}, j*1000)
})()
}
或者对代码改进一下
for(var i = 1; i <= 5; i++){
(function (j){
// 相当于let j = i, 存下了值
setTimeout(()=> {
console.log(j)
}, j*1000)
})(i)
}
立即执行函数会通过声明并立即执行来创建作用域,setTimeout会将立即执行函数在每次迭代中创建的作用域封闭起来!(封闭在每个迭代内部,每个迭代中都会有一个正确的值供我们访问) 因此,使用闭包可以解决这个问题.
根据立即执行函数创建作用域的解决方案分析,可以想到 let声明.
利用let声明劫持块作用域,并在这个作用域声明一个变量.
for(let i = 1; i <= 5; i++){
setTimeout(()=> {
console.log(i)
}, i*1000)
}
这样子同样可以得到期望的输出结果.
使用闭包的注意点
由于闭包会将变量保存在内存中,因此如果大量使用闭包可能会造成性能问题,如内存泄漏,通常的解决方式是在关闭函数之前将不使用的局部变量删除!