JS中有一个非常重要但又难以完全掌握的概念,那就是闭包。很多JS程序员自以为已经掌握了闭包,但实质上是一知半解,就像“JS中万物皆为对象”这个常见的错误说法一样,很多前端开发者到现在还固执己见。对于闭包,笔者现在也不敢说是完全掌握,但是希望通过自己对闭包的理解,让更多人对闭包的概念有一个更深刻的认识。
理解闭包的重要性,我想用一本书中的话来描述:“对于那些有一点JavaScript使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生”。
一、概念
1、定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
2、下面一段代码,清晰地展示了闭包:
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()依然持有对该作用域的引用,而这个引用就叫作闭包。
因此,在几微秒之后变量baz被实际调用(调用内部函数bar),不出意料它可以访问定义时的词法 作用域,因此它也可以如预期般访问变量a。 这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
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分配给全局变量
}
function bar() {
fn(); // 看,这就是闭包!
}
foo();
bar(); // 2
3、深入理解
闭包绝不仅仅是一个好玩的玩具。你已经写过的代码中一定到处都是闭包的身影。现在让我们来 搞懂这个事实。
function wait(message) {
setTimeout( function timer() {
console.log( message ); }, 1000 );
}
wait( "Hello, closure!" );
将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此 还保有对变量message的引用。
wait(..)执行1000毫秒后,它的内部作用域并不会消失,timer函数依然保有wait(..)作用域的闭 包。 深入到引擎的内部原理中,内置的工具函数setTimeout(..)持有对一个参数的引用,这个参数也许 叫作fn或者func,或者其他类似的名字。引擎会调用这个函数,在例子中就是内部的timer函数,而 词法作用域在这个过程中保持完整。 这就是闭包。
或者,如果你很熟悉jQuery(或者其他能说明这个问题的JavaScript框架),可以思考下面的代码:
function setupBot(name, selector) {
$( selector ).click(
function activator() {
console.log( "Activating:" + name ); } );
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );
本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类 型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通 信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
二.闭包的应用(或表现)
只知道闭包是什么还不够,要知道它的使用场景或说表现方式:
1、 要说明闭包,for循环是最常见的例子。
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout(
function timer() {
console.log( j );
}, j*1000 ); })( i );
}
for (var i=1; i<=5; i++) {
let j = i; // 是的,闭包的块作用域!
setTimeout( function timer() {
console.log( j ); }, j*1000 );
}
for循环头部的let声明还会有一个特殊的行 为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使 用上一个迭代结束时的值来初始化这个变量。
块作用域和闭包联手便可天下无敌。
2、 模块
还有其他的代码模式利用闭包的强大威力,但从表面上看,它们似乎与回调无关。下面一起来研究 其中最强大的一个:模块。
function CoolModule() {
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 = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
这个模式在JavaScript中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展 示的是其变体。
模块模式需要具备两个必要条件。
1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以 访问或者修改私有的状态。
一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用所返回 的,只有数据属性而没有闭包函数的对象并不是真正的模块。
三、总结:
闭包就好像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够 到达那里。但实际上它只是一个标准,显然就是关于如何在函数作为值按需传递的词法环境中书 写代码的。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生 了闭包。
如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循环中。但 同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。
模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至 少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
现在我们会发现代码中到处都有闭包存在,并且我们能够识别闭包然后用它来做一些有用的事!