当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行。
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
这是闭包吗?
技术上来讲,也许是。但根据前面的定义,确切地说并不是。因为并没有体现出bar记住了所在的词法作用域。我认为最准确地用来解释 bar() 对 a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却 是非常重要的一部分!)
下面我们来看一段代码,清晰地展示了闭包:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一 直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
因此,在几微秒之后变量 baz 被实际调用(调用内部函数 bar),不出意料它可以访问定义时的词法作用域,因此它也可以如预期般访问变量 a。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到 闭包。
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 妈妈快看呀,这就是闭包! bar函数居然访问到了foo函数的作用域。
}
把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部
作用域的闭包就可以观察到了,因为它能够访问 a了。等于在bar的作用域中读取到foo的作用域,所以产生了闭包。
来看看平常写代码遇到的闭包
function wait(msg) {
setTimeout(function timer(){
console.log(msg)
},1000)
}
wait("hello")
这怎么就是闭包了呢?
没关系,如果这个看不出来的话,我们不妨把代码变化一下
function wait(msg) {
function timer(){
console.log(msg)
}
setTimeout(timer,1000)
}
wait("hello")
如果还是理解不了的话。我们再来举出一个例子
function wait(msg) {
function timer(){
console.log(msg)
}
fn(timer)
}
function fn(fnc){
fnc();
}
wait("hello")
这就能看出来是如何使用的闭包了吧。
将一个内部函数(名为 timer)传递给 setTimeout(..)。timer 具有涵盖 wait(..) 作用域的闭包,因此还保有对变量 msg 的引用。
类似的还有
function setupBot(name, selector) {
$( selector ).click( function activator() {
console.log( "Activating: " + name );
} );
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );
使得 $( selector ).click 函数内部可以读取setupBot函数的作用域。
传递函数当然也可以是间接的。
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; //将baz分配给全局变量
}
function bar() {
fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包。本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!
循环和闭包
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
为什么打印5个6
仔细想一下,这好像又是显而易见的,延迟函数的回调会在循环结束时才执行。事实上, 当定时器运行时即使每个迭代中执行的是setTimeout(.., 0),所有的回调函数依然是在循 环结束后才会被执行,因此会每次输出一个 6 出来。
这里引伸出一个更深入的问题,代码中到底有什么缺陷导致它的行为同语义所暗示的不一 致呢?
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是 根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的, 但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
通过声明并立即执行一个函数来创建作用域。
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
构造函数和闭包
构造函数可以通过new 来调用产生对象。每次new都会产生一个新的作用域。这个作用域在哪呢?我们先看看new的过程是什么
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj)
JS中一般产生作用域的为函数块(当然let,catch块也可以产生作用域)。所以每一个Base.call(obj) 都会产生一个作用域。如果不存在闭包的话,这个作用域很快就会被释放。那怎么产生闭包呢?
function MyObject(){
//私有变量或私有函数(使用var 定义的变量)
var privateVariable=10;
// 特权方法 (挂在this上的变量)
this.publicMethod=function(){
privateVariable++;
};
}
var obj1 = new MyObject();
var obj2 = new MyObject();
obj1 和 obj2都可以通过publicMethod去修改privateVariable变量。但是这两个变量之间没有联系,是单独存在的。因为new的操作使obj1对象和obj2对象都有了个属性publicMethod可以访问到privateVariable变量,就使每次的MyObject作用域不能释放,造成了闭包。
在构造函数内部定义了所有私有变量和函数,又继续创建了能够访问这些私有成员的特权方法。能在构造函数中定义特权方法是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。
构造函数的闭包