(一)垃圾回收机制
函数中局部变量经历一个生命周期:
- 当我们定义一个变量时,会为它分配一个内存空间用来储存变量的值;
- 当我们读取或使用这个变量时,会使用它的内存空间;
- 使用完毕时会释放内存空间。
上面生命周期的最后一步,其实就是垃圾回收。JavaScript
有自动垃圾收集机制,就是找出那些不再继续使用的值,然后释放其占用的内存,那么怎么判断什么值不再使用呢,一般来说,当一个函数执行完毕,那么这个局部作用域内的变量就没有存在的必要了,它的内存空间就会释放,但是有一种情况可以阻止这一进程,那就是——闭包。
(二)闭包的概念
JavaScript
中闭包无处不在,你只需要能够识别并拥抱它。
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo() {
var a = 2;
// 函数bar() 的词法作用域能够访问foo() 的内部作用域。
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
// 在foo() 执行后,其返回值(也就是内部的bar() 函数)赋值给变量baz并调用baz()
// 实际上只是通过不同的标识符引用调用了内部的函数bar()。
// bar() 显然可以被正常执行,而且是在自己定义的词法作用域以外的地方执行了。
↑以上代码中,在foo()
执行后,通常会期待foo()
的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()
的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()
本身在使用。
拜bar()
所声明的位置所赐,它拥有涵盖foo()
内部作用域的闭包,使得该作用域能够一直存活,以供bar()
在之后任何时间进行引用。bar()
依然持有对该作用域的引用,而这个引用就叫作闭包。
现在我懂了
- 函数在定义时的词法作用域以外的地方被调用时,闭包使得函数可以继续访问定义时的词法作用域。
- 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
(三)循环和闭包
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(new Date(),i);
}, i*1000 );
}
console.log("end",new Date(),i); //为方便后边演示,这里加了打印end标志
↑以上代码,正常情况下,我们对for
循环中的这段代码行为的预期是分别输出数字1~5
,每秒一次,每次一个。
但实际上,for
循环中的代码在运行时会以每秒一次的频率输出五次 6
,结果如下:
为什么是每秒一次?每次都输出6呢?
那首先得对setTimeout
有正确的认识:setTimeout
的延迟不是绝对精确的
setTimeout
的意思是传递一个函数,延迟一段时候把该函数添加到队列当中,并不是立即执行;所以说如果当前正在运行的代码没有运行完,即使延迟的时间已经过完,该函数会等待到函数队列中前面所有的函数运行完毕之后才会运行;也就是说所有传递给setTimeout
的回调方法都会在整个环境下的所有代码运行完毕之后执行;
setTimeout(function(){
console.log("here");
}, 0);
var i = 0;
//具体数值根据你的计算机CPU来决定,达到延迟效果就好
while (i < 3000000000) {
i ++;
}
console.log("test");
// ↑运行上面代码,结果为在过了一段时间之后,先打印了test,然后才是here。
// 而且需要注意的是,上面的代码写的是setTimeout(..,0),
// 如果按照之前错误地将setTimeout函数理解为延迟一段时间执行,那这里把时间赋为0岂不是马上执行了?
// 而实验结论则印证了上面“setTimeout的意思是传递一个函数,延迟一段时间把该函数添加到队列中,并不是立即执行”的结论。
再回到最初的那个问题,刚进入for
循环的时候,i
为1
,所以相对于现在延迟一秒将timer
函数添加到队列当中,然后for
循环继续(而并不是等一秒再继续循环),进行第二次循环,这时候i
为2
,所以相对于现在延迟两秒将timer
函数送进队列。以此类推,for
循环的时间可以忽略不计,timer
函数就以每秒一次的频率执行。
由于setTimeout
是异步的,所以在for
循环执行结束后(i
的值为6
),会接着执行后面的代码,等过了1s
后,最后一行代码console.log("end",new Date(),i);
早就执行完了,所以会先打印最后一行代码i
的值6
,然后以每秒一次的频率输出五次 6
。
那么问题来了?
代码中到底有什么缺陷导致它的行为同语义所暗示的不一 致呢?
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i
的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
想要到达预期的结果,就得想办法让循环的过程中每个迭代都有一个闭包作用域。
1. 立即执行函数
for (var i = 1 ; i <= 5 ; ++i) {
(function(j){
setTimeout( function timer(){
console.log(j);
} , j*1000);
})(i);
}
// 这里将i立即传进去,形成了封闭的5个函数
// timer只能访问到传进去的那个i,也就是我们所需的i
2. ES6
中的let
与块作用域
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
// for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,
// 事实上它将其重新绑定到了循环的每一个迭代中,
// 确保使用上一个循环迭代结束时的值重新进行赋值。
(四)模块
1. 模块模式
模块模式需要具备两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
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
// doSomething()和doAnother()函数具有涵盖模块实例内部作用域的闭包(通过调用CoolModule()实现)
↑以上代码中有一个叫作CoolModule()
的独立的模块创建器,可以被调用任意多次,每次调用都会创建一个新的模块实例。当只需要一个实例时,可以对这个模式进行简单的改进来实现单例模式:
var foo = (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
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
2. 现代的模块机制
// 模块API
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
} );
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}
return {
awesome: awesome
};
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(bar.hello("hippo")); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
"foo"
和 "bar"
模块都是通过一个返回公共 API
的函数来定义的。"foo"
甚至接受 "bar"
的 示例作为依赖参数,并能相应地使用它。
3. 未来的模块机制
ES6
中import
可以将一个模块中的一个或多个API
导入到当前作用域中,并分别绑定在一个变量上;export
会将当前模块的一个标识符(变量、函数)导出为公共API
。
小结
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
模块有两个主要特征:
- 为创建内部作用域而调用了一个包装函数;
- 包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。