在文章【JS:深入理解JavaScript-执行上下文】中介绍了代码在执行栈是如何运行的,假设有如下代码:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
bar();
}
foo()
引擎在执行这段代码的步骤如下:
- 1:创建一个新的执行上下文(Execution Context)
- 2:创建一个新的词法环境(Lexical Environment)
- 3:把LexicalEnvironment和VariableEnvironment指向新创建的词法环境
- 4:把这个执行上下文压入执行栈并成为正在运行的执行上下文
- 5:执行代码
- 6:执行结束后,把这个执行上下文弹出执行栈
代码在执行完1-4步以后,整个环境看起来是这样的:
执行第五步,执行到foo会先给变量a
赋值,然后给bar方法创建一个新的执行上下文,然后再执行console.log(a)
:
执行第六步,foo
bar
执行完后被弹出执行栈,这两个function对象(红色区域1和2)还在内存中,等待垃圾回收。
在执行完上面的代码以后,可以看到foo
bar
的词法环境访问链路断掉了,虽然它们还在内存了(红色区域1和2),但是我们再也没办法访问这两个词法环境里的变量。
这时候如果还想访问foo
bar
的词法环境,比如还想用a的值,我们把代码改一下:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();
运行baz()
会输出2(2是foo词法环境里的值),也就是变量a在foo词法环境之外被访问了,这就是闭包。
正常情况下,在方法foo执行完以后,foo的执行上下文被弹出执行栈,它的词法环境链路也就失联了。我们知道每个方法在执行的时候都会创建一个新的执行上下文,同时也会创建它们自己的词法环境,每个方法的词法环境里有一个scope
会保存(指向)它上一层的词法环境。 那么foo方法执行完以后返回bar,这个bar的scope里还保留着整个foo方法的词法环境,那么在执行baz()
的时候也就是执行bar()
,这样就可以访问失联的foo方法的词法作用域
,也就是可以拿到变量a
的值。
闭包就是指:执行完的执行上下文
被弹出执行栈,它的词法环境处于失联状态,后续的执行上下文没办法直接访问这个失联的词法环境。在这种情况下还保留了对那个词法环境的引用
,从而可以通过这个引用
去访问失联的词法环境,这个引用
就是闭包。
其实我们每天写的代码,基本会用到闭包,JS也有很多闭包的应用有以下几种方式:
- 第一种:
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // closure!
- 第二种:
function foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); // closure! }
- 第三种:
var fn; function foo() { var a = 2; function baz() { console.log( a ); } fn = baz; // assign `baz` to global variable } function bar() { fn(); // closure! } foo(); bar(); // 2
- 第四种:
function wait(message) { setTimeout( function timer(){ console.log( message ); }, 1000 ); } wait( "Hello, closure!" ); // 打印出 hello closure! 回调函数的message是wait方法作用域的值。
需要注意的是,如果代码写成这样:
function wait(message) { setTimeout( function timer(){ console.log( this.message ); }, 1000 ); } wait( "Hello, closure!" ); // 打印出undefined, 回调函数的this值的是全局变量,全局变量没有这个值,所以是undefined。
- 第五种是模块化