作用域闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
词法作用域:定义在词法阶段的作用域,在写代码时将变量和块作用域写在哪里来决定。(还有动态作用域)
function foo(){
var a = 2;
function bar(){
console.log(a);//2
}
bar();
}
foo();
上面的例子中bar()被封闭在了foo()内部。函数bar()具有一个涵盖foo()作用域的闭包(事实上它涵盖了它能访问的所有作用域,例如全局作用域)。
再看一个例子如下:
function foo(){
var a = 2;
function bar(){
console.log(a);//2
}
return bar;
}
var baz = foo();
baz();//2
函数bar()的词法作用域能够访问foo()内部作用域。函数bar()本身被当做一个值类型进行传递。
在foo()执行后,返回值(bar()函数)赋值给变量baz并调用baz(),实际上就是通过不同的标识符引用调用了内部的函数bar()。
bar()可以被正常执行,但是它是在自己定义的词法作用域之外的地方被执行的。
在foo()执行之后,通常会期待foo()的整个内部作用域被销毁,但是闭包可以阻止这个。
在上面的例子中,foo()的内部作用域依然存在,所以没有被回收,谁在使用?是bar()本身在使用。
由于bar()函数在foo()函数内部声明,它能够使得该作用域一直存活。bar()函数一直持有对于该作用域的引用,这个引用就叫做闭包。
变量baz被实际调用(就是内部函数bar()),它可以访问在它定义时的词法作用域,所以它可以访问变量a。
这个函数在定义时的词法作用域之外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
看另一个例子:
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
fn();//这就是一个闭包
}
把内部函数baz传递给bar(),当调用这个内部函数(fn)时,可以访问当foo()的内部作用域,就可以访问到a。
无论通过什么手段将内部的函数传递到所在的词法作用域之外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
闭包的部分应用分析
function wait(message){
setTimeout(function timer(){
console.log(message);
},1000);
}
wait('hello');
在上面的例子中,名为timer的内部函数作为传参传递给了setTimeout(..),timer函数持有wait函数内部作用域的闭包,它可以对message变量进行引用。
wait函数在执行1000ms后,它的内部作用域并不会消失,timer函数依然持有wait函数内部作用域的闭包。
setTimeout函数持有对一个参数的引用,引擎会调用这个参数,该例子中就是timer函数,而词法作用域在这个过程中会保持完整。
另一个例子:
function setupBot(name,selector){
$(selector).click(function activator(){
console.log(name);
})
}
setupBot('Bot1','#bot_1');
setupBot('Bot2','#bot_2');
这个例子是在jQuery中的应用。这实际上也是闭包。
在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调,就相当于使用了闭包。
IIFE是不是闭包?
//to do …
循环和闭包
for (var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},1000);
}
每秒一次的频率输出五次6。
延迟函数的回调会在循环结束的时候才执行。循环结束的条件时I=6,执行回调的时候已经是i的值为6了,所有会输出五次6。
我们对于这段代码的预期应该是输出1~5,但为什么会出现这样的结果?
因为在预期中,我们试图假设循环中的每个迭代在运行的时候都会给自己获取一个i的副本。但是,根据作用域的工作原理,实际情况是尽管循环中的五个函数是分别在各自的迭代中定义的,但是他们都被封闭在一个共享的全局作用于下,实际上只有一个i。因此这五个函数共用同一个i的引用。
如何实现预期的效果?
我们需要给循环中的每一个迭代增加一个闭包作用域,让他们各自互不影响。
尝试一下IIFE:
for (var i=1;i<=5;i++){
(function(){
setTimeout(function timer(){
console.log(i);
},1000);
})();
}
这样结果还是输出五次6。
为什么不行?
因为我们新增的这个立即执行函数它的作用域是空的。我们在获取对i的引用时,还是会到外部的作用域取值。
进行如下的修改:
for (var i=1;i<=5;i++){
(function(){
var j = i;
setTimeout(function timer(){
console.log(j);
},1000);
})();
}
这样输出结果就是预期的1,2,3,4,5。
还有一种写法和上面的结果一样:
for (var i=1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},1000);
})(i);
}
不定义变量j,直接将j作为立即执行函数的传参,而此时我们将i作为参数传进去,和上面的代码结果一致。
此时IIFE会为循环的每一个迭代都创建一个新的作用域,将延迟函数的回调封闭在每一个迭代中。
模块
function CoolModules(){
var something = "cool";
var another = [1,2,3];
function doSomething(){
console.log(something);
}
function doAnother(){
console.log(another.join('!'));
}
return {
doSomething:doSomething,
doAnother:doAnother
}
}
var foo = CoolModules();
foo.doSomething();//cool
foo.doAnother();//1!2!3
//to do …
两个特征:
- 为创建内部作用域而调用了一个包装函数
- 包装函数的返回值必须至少包括一个对内部函数的引用