当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz(); //2 -------->这就是闭包的效果
函数bar的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。
在foo()执行过后,其返回值(也就是内部的bar函数)赋值给变量baz并调用baz(), 实际上只是通过不同的标识符引用了内部的函数bar()。
bar()显然可以被正常执行。但是在这个例子中,他在自己定义的词法作用域以外的地方执行。
在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然处在,以此没有被回收。谁在使用这个内部作用域? 原来是bar本身在使用。
拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得改作用域能够一直存活,以供bar在之后任何时间进行引用。
bar()依然持有对该作用域的引用,而这个就叫做闭包。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
function foo(){
var a = 10;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
fn(); //这就是闭包
}
传递函数当然也可以是间接的:
var fn;
function foo(){
var a = 10;
function baz(){
console.log(a);
}
fn = baz; //将baz分配给全局变量
}
function bar(){
fn(); //这就是闭包
}
foo();
baz(); //2
无论通过何种手段将内部函数传递到所在的词法作用域以外,他就会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
那么再看下面的一个例子:
function wait(msg){setTimeout(function timer(){
console.log(msg);
}, 1000)
}
wait("hello,closure");
将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此还包有对变量msg的引用。
或者,如果你很熟悉jQuery,可以思考下面的代码:
function setBot(name, selector){
$(selector).click(function activetor(){
console.log("activetor" + name);
})
}
其实是将内部函数activetor传递到所在的词法作用域之外执行,就产生了闭包。
在定时器,事件监听器,ajax请求,夸窗口通信,WebWorkor或者任何其他的异步任务中,只要使用了回调函数,实际上就是在使用闭包!!!
循环和闭包
for(var i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
}, i*1000)
}
这段代码在运行时会以每秒一次的频率输出五次6; 原因如下:
setTimeout是异步任务,而for循环是同步任务,for循环一瞬间执行完,与此同时浏览器会启动五个定时器进行计时,依次在1,2,3,4,5秒之后将timer函数放到异步任务队列里面,在执行timer函数的时候,for循环早已结束,而结束时i = 6; timer持有对原始作用域的引用,而原始作用域的i=6, 所有会输出五次6;
那么接下来再看下面的这个例子:
for(var i = 1; i <= 5; i++){
(function(j){
setTimeout(function timer(){
console.log(j);
}, j*1000)
})(i);
}
上面的代码执行结果为每隔一秒以次输出1,2,3,4,5
timer持有对原始作用域的引用,而(function(){})()立即执行函数会形成自己独立的作用域,在这个作用域内部,j的值依次为1,2,3,4,5
换句话说,每次迭代我们都需要一个块级作用域。ES6里面let可以用来劫持块级作用域,并且在这个作用域中声明一个变量。本质上这是将一个块转换成一个可以被关闭的作用域。
for(var i = 1; i <= 5; i++){
let j = i;
setTimeout(function timer(){
console.log(j);
}, j*1000);
}
当然了,你还可以这样写:
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
}, i*1000);
}
注意:let在每次迭代都会声明。