在文章【深入理解 JavaScript 的执行上下文】中介绍了代码在执行栈是如何运行的,假设有如下代码:
function foo() { var a = 2; function bar() { console.log(a); } bar(); } foo()
引擎在执行这段代码的步骤如下:
代码在执行完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。
文章转自:https://limeii.github.io/2019/05/js-closures/ 作者:Li Mei
欢迎关注「前端达人」公众号