我们先来思考一个问题:怎么读取一个函数里的私有变量?
众所周知,javascript具有函数作用域,也即是说函数内部可以读取外部的变量,但外部是无法读取函数内部的私有变量的。因此如果我们在这个函数(暂且叫做outer
)里再添加一个内部函数inner
,inner
就能读取到outer
的私有变量。如果我们再把inner
当作返回值返回,不就相当于间接得到了outer
的私有变量了嘛!而事实上,这就是闭包。
官方对闭包的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。闭包的特点:
1.作为一个函数变量的一个引用,当函数返回时,其处于激活状态。
2.一个闭包就是当一个函数返回时,一个没有释放资源的栈区。
这样的解释有点抽象,从狭义上来讲其实可以理解为 “一个函数引用了另一个函数作用域里的变量”。
我们来看下面的例子:
- function closure(){
- var name = "enm";
- return {
- getStr:function(){
- return name;
- }
- }
- }
- var builder = new closure();
- builder.name;//undefined
- name;//undefined,这里是访问不了function的私有变量的
- console.log(builder.getStr()); //返回了enm
上面构造了一个闭包,这个闭包都维持着对外部作用域的引用,因此不管在哪调用总是能够访问函数中的变量。在一个函数内部定义的函数,会将外部函数的活跃对象添加到自己的作用域链中,因此上面实例中通过内部函数能够访问外部函数的属性,这也是javascript模拟私有变量的一种方式。
闭包经典问题
我们来看看下面经典的例子,相信任何一位初学者在接触闭包时都会遇到这个问题:
- function timeManage() {
- for (var i = 0; i < 5; i++) {
- setTimeout(function() {
- console.log(i);
- },1000)
- };
- }
上面的程序并没有按照我们预期的输入1-5的数字,而是5次全部输出了5。我们先来看解决办法:
- function timeManage() {
- for (var i = 0; i < 5; i++) {
- (function(num) {
- setTimeout(function() {
- console.log(num);
- }, 1000);
- })(i);
- }
- }
或者在闭包匿名函数中再返回一个匿名函数赋值:
- function timeManage() {
- for (var i = 0; i < 10; i++) {
- setTimeout((function(e) {
- return function() {
- console.log(e);
- }
- })(i), 1000)
- }
- }
- //timeManager();输出1,2,3,4,5
我再尝试另一种方法,我把它叫做“消除延迟方法”:
- function timeManage() {
- function foo() {
- console.log(i);
- }
- for (var i = 0; i < 5; i++) {
- foo()
- };
- }
- timeManage();//输出1,2,3,4,5
继续看另一个例子
- function createClosure(){
- var result = [];
- for (var i = 0; i < 5; i++) {
- result[i] = function(){
- return i;
- }
- }
- return result;
- }
调用createClosure()[0]()返回的是5,createClosure()[4]()返回值仍然是5。原因我们最后再说,因为现在我自己的理解跟网上其他人的理解有出入。我们先来看看怎么解决。
- function createClosure() {
- var result = [];
- for (var i = 0; i < 5; i++) {
- result[i] = function(num) {
- return function() {
- console.log(num);
- }
- }(i);
- }
- return result;
- }
- //createClosure()[1]()输出1;createClosure()[2]()输出2
再使用我自己的“消除延迟”方法看看
- function createClosure(){
- var result = [];
- for (var i = 0; i < 5; i++) {
- result[i] = (function(){
- return i;
- })();
- }
- return result;
- }
- var w = new createClosure();
- w; //成功输出[0,1,2,3,4]
以上提供的解决办法,无论是匿名包裹器
还是通过嵌套匿名函数
的方式,原理上都是将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于返回num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。而第三种方法消除延迟
则是先定义函数,或者定义立即执行函数,然后在for循环中直接调用函数,这也是一个闭包,也依然保持对外部变量i的访问,但是不会出现我们说的问题
- function createClosure(){
- var result = [];
- for (var i = 0; i < 5; i++) {
- result[i] = function(){
- return i;
- };
- }
- return result;
- }
- var w = new createClosure();
- w; //输出 [object Array][function, function, function, function, function]
- w[0](); //5
这里就很明显了,我们把函数表达式直接赋值给result[i]
,此时函数并没有执行,看看我们控制台输出的结果就知道了,w只是一个包含了函数表达式的Array,而不是具体数值。而等到我们调用w[0]()
的时候,函数才开始执行计算,此时再调用外部的变量i(循环已完毕,i
== 5),所以w[0]()
为5。到此,该问题就解决了。
事实上,这个例子的本意是用来说明闭包保持对外部函数变量的引用(reference),而不是复制值(copy)。因此当外部变量改变时,内部函数的引用也会跟着改变。但是,这个例子很容易让人迷糊,让初学者摸不着头脑,以为闭包非常的magic,以为是闭包的奇异功能导致了这个奇怪现象的发生。所以,有人解释出现这个问题的原因是由于作用域链机制的影响,闭包只能取得内部函数的最后一个值,这样说没有错,因为你执行完循环再调用函数,函数引用同一个i,那么结果肯定是最后修改的值5。但这样解释会让这个问题变得迷糊,特别是对初学者而言,他们会混乱,会觉得闭包很深奥很不可理解。因为其实本质上这个例子是通过setTimeout函数故意延迟了函数的执行,通过我第三种解决方法可以看出,假如每一次循环都能立即执行函数,那么是完全可以输出每一个i的正确值的。也就是说,其实这个问题的容易让人迷糊的原因是函数没有立即执行,而不能说是闭包产生的结果。
闭包中的this
闭包中的this 在闭包中使用this时要特别注意,稍微不慎可能会引起问题。通常我们理解this对象是运行时基于函数绑定的,全局函数中this对象就是window对象,而当函数作为对象中的一个方法调用时,this等于这个对象(TODO 关于this做一次整理)。由于匿名函数的作用域是全局性的,因此闭包的this通常指向全局对象window:
- var scope = "global";
- var object = {
- scope:"local",
- getScope:function(){
- return function(){
- return this.scope;
- }
- }
- }
调用object.getScope()()返回值为global而不是我们预期的local,前面我们说过闭包中内部匿名函数会携带外部函数的作用域,那为什么没有取得外部函数的this呢?每个函数在被调用时,都会自动创建this和arguments,内部匿名函数在查找时,搜索到活跃对象中存在我们想要的变量,因此停止向外部函数中的查找,也就永远不可能直接访问外部函数中的变量了。总之,在闭包中函数作为某个对象的方法调用时,要特别注意,该方法内部匿名函数的this指向的是全局变量。 幸运的是我们可以很简单的解决这个问题,只需要把外部函数作用域的this存放到一个闭包能访问的变量里面即可:
- var scope = "global";
- var object = {
- scope:"local",
- getScope:function(){
- var that = this;
- return function(){
- return that.scope;
- }
- }
- }
object.getScope()()返回值为local。
内存与性能 由于闭包中包含与函数运行期上下文相同的作用域链引用,因此,会产生一定的负面作用,当函数中活跃对象和运行期上下文销毁时,由于必要仍存在对活跃对象的引用,导致活跃对象无法销毁,这意味着闭包比普通函数占用更多的内存空间,在IE浏览器下还可能会导致内存泄漏的问题,如下:
- function bindEvent(){
- var target = document.getElementById("elem");
- target.onclick = function(){
- console.log(target.name);
- }
- }
上面例子中匿名函数对外部对象target产生一个引用,只要是匿名函数存在,这个引用就不会消失,外部函数的target对象也不会被销毁,这就产生了一个循环引用。解决方案是通过创建target.name副本减少对外部变量的循环引用以及手动重置对象:
- function bindEvent(){
- var target = document.getElementById("elem");
- var name = target.name;
- target.onclick = function(){
- console.log(name);
- }
- target = null;
- }
闭包中如果存在对外部变量的访问,无疑增加了标识符的查找路径,在一定的情况下,这也会造成性能方面的损失。解决此类问题的办法我们前面也曾提到过:尽量将外部变量存入到局部变量中,减少作用域链的查找长度。
总结:闭包不是javascript独有的特性,但是在javascript中有其独特的表现形式,使用闭包我们可以在javascript中定义一些私有变量,甚至模仿出块级作用域,但闭包在使用过程中,存在的问题我们也需要了解,这样才能避免不必要问题的出现。