初学JavaScript的时候,我在学习闭包上,走了不少弯路。而此次从新回过头来对基础知识进行梳理,要讲清楚闭包,也是一个很是大的挑战。javascript
闭包有多重要?若是你是初入前端的朋友,我没有办法直观的告诉你闭包在实际开发中的无处不在,可是我能够告诉你,前端面试,必问闭包。面试官们经常用对闭包的了解程度来断定面试者的基础水平,保守估计,10个前端面试者,至少5个都死在闭包上。前端
但是为何,闭包如此重要,仍是有那么多人没有搞清楚呢?是由于你们不肯意学习吗?还真不是,而是咱们经过搜索找到的大部分讲解闭包的中文文章,都没有清晰明了的把闭包讲解清楚。要么浅尝辄止,要么高深莫测,要么干脆就直接乱说一通。包括我本身曾经也写过一篇关于闭包的总结,回头一看,不忍直视[捂脸]。java
所以本文的目的就在于,可以清晰明了得把闭包说清楚,让读者老爷们看了以后,就把闭包给完全学会了,而不是似懂非懂。面试
在详细讲解做用域链以前,我默认你已经大概明白了JavaScript中的下面这些重要概念。这些概念将会很是有帮助。算法
做用域chrome
在JavaScript中,咱们能够将做用域定义为一套规则,这套规则用来管理引擎如何在当前做用域以及嵌套的子做用域中根据标识符名称进行变量查找。编程
这里的标识符,指的是变量名或者函数名设计模式
JavaScript中只有全局做用域与函数做用域(由于eval咱们平时开发中几乎不会用到它,这里不讨论)。数组
做用域与执行上下文是彻底不一样的两个概念。我知道不少人会混淆他们,可是必定要仔细区分。浏览器
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段做用域规则会肯定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段建立。
做用域链
回顾一下上一篇文章咱们分析的执行上下文的生命周期,以下图。
咱们发现,做用域链是在执行上下文的建立阶段生成的。这个就奇怪了。上面咱们刚刚说做用域在编译阶段肯定规则,但是为何做用域链却在执行阶段肯定呢?
之全部有这个疑问,是由于你们对做用域和做用域链有一个误解。咱们上面说了,做用域是一套规则,那么做用域链是什么呢?是这套规则的具体实现。因此这就是做用域与做用域链的关系,相信你们都应该明白了吧。
咱们知道函数在调用激活时,会开始建立对应的执行上下文,在执行上下文生成的过程当中,变量对象,做用域链,以及this的值会分别被肯定。以前一篇文章咱们详细说明了变量对象,而这里,咱们将详细说明做用域链。
做用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
为了帮助你们理解做用域链,我咱们先结合一个例子,以及相应的图示来讲明。
var a = 20; function test() { var b = a + 10; function innerTest() { var c = 10; return b + c; } return innerTest(); } test();
在上面的例子中,全局,函数test,函数innerTest的执行上下文前后建立。咱们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的做用域链,则同时包含了这三个变量对象,因此innerTest的执行上下文可以下表示。
innerTestEC = {
VO: {...}, // 变量对象 scopeChain: [VO(innerTest), VO(test), VO(global)], // 做用域链 this: {} }
是的,你没有看错,咱们能够直接用一个数组来表示做用域链,数组的第一项scopeChain[0]为做用域链的最前端,而数组的最后一项,为做用域链的最末端,全部的最末端都为全局变量对象。
不少人会误解为当前做用域与上层做用域为包含关系,但其实并非。以最前端为起点,最末端为终点的单方向通道我认为是更加贴切的形容。如图。
注意,由于变量对象在执行上下文进入执行阶段时,就变成了活动对象,这一点在上一篇文章中已经讲过,所以图中使用了AO来表示。Active Object
是的,做用域链是由一系列变量对象组成,咱们能够在这个单向通道中,查询变量对象中的标识符,这样就能够访问到上一层做用域中的变量了。
对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来讲,理解闭包能够看做是某种意义上的重生,突破闭包的瓶颈可使你功力大增。
先直截了当的抛出闭包的定义:当函数能够记住并访问所在的做用域(全局做用域除外)时,就产生了闭包,即便函数是在当前做用域以外执行。
简单来讲,假设函数A在函数B的内部进行定义了,而且当函数A在执行时,访问了函数B内部的变量对象,那么B就是一个闭包。
很是抱歉以前对于闭包定义的描述有一些不许确,如今已经改过,但愿收藏文章的同窗再看到的时候能看到吧,对不起你们了。
关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。
而咱们知道,函数的执行上下文,在执行完毕以后,生命周期结束,那么该函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。但是闭包的存在,会阻止这一过程。
先来一个简单的例子。
var fn = null; function foo() { var a = 2; function innnerFoo() { console.log(a); } fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn } function bar() { fn(); // 此处的保留的innerFoo的引用 } foo(); bar(); // 2
在上面的例子中,foo()
执行完毕以后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。可是经过fn = innerFoo
,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,致使了foo的变量对象,也被保留了下来。因而,函数fn在函数bar内部执行时,依然能够访问这个被保留下来的变量对象。因此此刻仍然可以访问到变量a的值。
这样,咱们就能够称foo为闭包。
下图展现了闭包fn的做用域链。
咱们能够在chrome浏览器的开发者工具中查看这段代码运行时产生的函数调用栈与做用域链的生成状况。以下图。
在上面的图中,红色箭头所指的正是闭包。其中Call Stack为当前的函数调用栈,Scope为当前正在被执行的函数的做用域链,Local为当前的局部变量。
因此,经过闭包,咱们能够在其余的执行上下文中,访问到函数的内部变量。好比在上面的例子中,咱们在函数bar的执行环境中访问到了函数foo的a变量。我的认为,从应用层面,这是闭包最重要的特性。利用这个特性,咱们能够实现不少有意思的东西。
不过读者老爷们须要注意的是,虽然例子中的闭包被保存在了全局变量中,可是闭包的做用域链并不会发生任何改变。在闭包中,能访问到的变量,仍然是做用域链上可以查询到的变量。
对上面的例子稍做修改,若是咱们在函数bar中声明一个变量c,并在闭包fn中试图访问该变量,运行结果会抛出错误。
var fn = null; function foo() { var a = 2; function innnerFoo() { console.log(c); // 在这里,试图访问函数bar中的c变量,会抛出错误 console.log(a); } fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn } function bar() { var c = 100; fn(); // 此处的保留的innerFoo的引用 } foo(); bar();
闭包的应用场景
接下来,咱们来总结下,闭包的经常使用场景。
咱们知道setTimeout的第一个参数是一个函数,第二个参数则是延迟的时间。在下面例子中,
function fn() { console.log('this is test.') } var timer = setTimeout(fn, 1000); console.log(timer);
执行上面的代码,变量timer的值,会当即输出出来,表示setTimeout这个函数自己已经执行完毕了。可是一秒钟以后,fn才会被执行。这是为何?
按道理来讲,既然fn被做为参数传入了setTimeout中,那么fn将会被保存在setTimeout变量对象中,setTimeout执行完毕以后,它的变量对象也就不存在了。但是事实上并非这样。至少在这一秒钟的事件里,它仍然是存在的。这正是由于闭包。
很显然,这是在函数的内部实现中,setTimeout经过特殊的方式,保留了fn的引用,让setTimeout的变量对象,并无在其执行完毕后被垃圾收集器回收。所以setTimeout执行结束后一秒,咱们任然可以执行fn函数。
在函数式编程中,利用闭包可以实现不少炫酷的功能,柯里化算是其中一种。关于柯里化,我会在之后详解函数式编程的时候仔细总结。
在我看来,模块是闭包最强大的一个应用场景。若是你是初学者,对于模块的了解能够暂时不用放在心上,由于理解模块须要更多的基础知识。可是若是你已经有了不少JavaScript的使用经验,在完全了解了闭包以后,不妨借助本文介绍的做用域链与闭包的思路,从新理一理关于模块的知识。这对于咱们理解各类各样的设计模式具备莫大的帮助。
(function () { var a = 10; var b = 20; function add(num1, num2) { var num1 = !!num1 ? num1 : a; var num2 = !!num2 ? num2 : b; return num1 + num2; } window.add = add; })(); add(10, 20);
在上面的例子中,我使用函数自执行的方式,建立了一个模块。方法add被做为一个闭包,对外暴露了一个公共方法。而变量a,b被做为私有变量。在面向对象的开发中,咱们经常须要考虑是将变量做为私有变量,仍是放在构造函数中的this中,所以理解闭包,以及原型链是一个很是重要的事情。模块十分重要,所以我会在之后的文章专门介绍,这里就暂时很少说啦。
为了验证本身有没有搞懂做用域链与闭包,这里留下一个经典的思考题,经常也会在面试中被问到。
利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); }
for (var i = 1; i <= 5; i++) { (function (i) { setTimeout(function timer() { console.log(i); }, i * 1000); })(i); }
for (var i = 1; i <= 5; i++) { setTimeout(function (i) { return function () { console.log(i); } }(i), i * 1000); }