介绍
在之前的文章中连续介绍了作用域的知识,有了这些知识储备,我们就来学习本节的内容作用域闭包
。回忆工作这几年,大量使用JavaScript
或多或少也在运用闭包,现在我们试着从理论角度来讨论下闭包。
什么是闭包?
遇到这种问题,第一时间看mdn文档。官方文档如下解释:
闭包是由函数以及创建该函数的词法环境组合而成。
这个描述过于抽象不利于理解先放一边,我们先来看一段代码:
function foo() {
var a = 1;
function bar() {
console.log( a ); // 1
}
bar();
}
foo();
复制代码
这是一段嵌套函数代码,foo
函数中声名了一个bar
函数。根据我们之前学习作用域相关的知识。词法作用域是由代码声明的位置决定的,foo
函数其中有两个标识符a
、bar
,bar
函数可以访问在其外部声明的变量也就是a
。
再来看另一段代码:
function foo() {
var a = 1;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 1
复制代码
这段代码直观的展示了闭包的效果。我们将bar
函数本身当作一个函数对象返回。在运行foo
函数后,其返回值(也就是bar
函数的引用)赋值给变量baz
并调用baz()
,实际上就是通过不同的标识符引用调用了内部函数bar
。
在foo()
执行后,通常会期待foo()
的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()
的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()
本身在使用。
bar()
所声明的位置,它拥有涵盖foo()
内部作用域的闭包,使得该作用域能够一直存活,以供bar()
在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行。
循环与闭包
for
循环是最常见的例子:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}
复制代码
我们对这段代码的预期结果分别输出数字1-5,每秒一次,每次一个。但这段代码在运行时会以每秒一次的频率输出五次6。延迟函数的回调会在循环结束时才执行。这显然不是我们想要的结果。
我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i
的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i
。
我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域,例如:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
复制代码
在迭代内使用IIFE
会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
ES6中let
声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量,例如:
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}
复制代码
模块与闭包
我们利用闭包的强大功能,可以实现一个JavaScript
模块,例如:
function module() {
var something = "module";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = module();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
复制代码
首先,module
只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。
然后module
函数返回一个对象。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共API。
这个对象类型的返回值最终被赋值给外部的变量foo
,然后就可以通过它来访问API中的属性方法,比如foo.doSomething()
。
可以对这个模式进行简单的 改进来实现单例模式:
var foo = (function module() {
var something = "module";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
复制代码
小结
闭包本质也是作用域的产物,闭包的规律也是作用域的规律。本章也是简单的介绍了一下闭包,更多更深入的内容还是来源我们项目中的代码。