function a(){ var n = 0; this.inc = function () { n++; console.log(n); }; } var c = new a(); c.inc(); //控制台输出1 c.inc(); //控制台输出2
什么是闭包?这就是闭包!!有权访问另一个函数作用域内变量的函数都是闭包。当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
这里 inc 函数访问了构造函数 a 里面的变量 n,所以形成了一个闭包。
function a(){ var n = 0; function inc(){ n++; console.log(n); } return inc; } var c = a(); c(); //控制台输出1 c(); //控制台输出2
执行过程:
var c=a();这里的a()返回的是函数inc,那这句等同于var c = inc;c()相当于inc();函数名只是一个标识(指向函数的指针),而()才是执行函数。
为什么要用闭包?
js的每个函数都是一个个小黑屋,它可以获取外界信息,但是外界却无法直接看到里面的内容。将变量 n 放进小黑屋里,除了 inc 函数之外,没有其他办法能接触到变量 n,而且在函数 a 外定义同名的变量 n 也是互不影响的,这就是所谓的增强“封装性”。而之所以要用 return 返回函数标识 inc,是因为在 a 函数外部无法直接调用 inc 函数,所以 return inc 与外部联系起来,第一个代码中的 this 也是将 inc 与外部联系起来而已。
常见的陷阱
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ return i; }; } return result; } var funcs = createFunctions(); for (var i=0; i < funcs.length; i++){ console.log(funcs[i]()); }
原本想要输出0~9,但是最后的结果是10个10,为什么呢?
这里的陷阱就是:函数带()才是执行函数! 单纯的一句 var f = function() { alert('Hi'); }; 是不会弹窗的,后面接一句 f(); 才会执行函数内部的代码。上面代码翻译一下就是:
var result = new Array(), i; result[0] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换! result[1] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换! ... result[9] = function(){ return i; }; //没执行函数,函数内部不变,不能将函数内的i替换! i = 10; funcs = result; result = null; console.log(i); // funcs[0]()就是执行 return i 语句,就是返回10 console.log(i); // funcs[1]()就是执行 return i 语句,就是返回10 ... console.log(i); // funcs[9]()就是执行 return i 语句,就是返回10
为什么只垃圾回收了 result,但却不收了 i 呢? 因为 i 还在被 function 引用着啊。这就是闭包的神奇之处,内部作用域依然存在,因此没有被回收。result依然持有对该作用域的引用,这个引用就叫做闭包!
更改方法:
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(x){ return function(){ return x; } }(i); } return result; } var funcs = createFunctions(); for (var i=0; i < funcs.length; i++){ console.log(funcs[i]()); }
没有直接把闭包赋值给数组,而是定义了一个匿名函数,并通过立即执行该匿名函数的结果赋值给数组,并带了for循环的参数i进去,让x能找到传入的参数值为0-10,这就解释了函数参数是按值传递的,所以会将变量i的当前值复制给参数x。而这个匿名函数内部又创建并返回了一个访问x的闭包。这样以来result数组中的每个函数都有自己x变量的一个副本,所以会符合我们的预期输出不同的值。
闭包的应用场景
闭包应用的两种情况:函数作为返回值,函数作为参数传递
1、函数作为返回值
function fn(){ var max=10; return function bar(x){ if (x>max){ console.log(x); } }; } var f1=fn(); f1(15);
bar函数作为返回值,赋值给f1变量。执行f1(15)时,用到了fn作用域下的max变量的值。
2、函数作为参数被传递
var max = 10; var fn=function(x){ if(x>max){ console.log(x); } }; (function(f){ var max = 100; f(15); })(fn);
fn函数作为一个参数被传递进入另一个函数,赋值给f参数。执行f(15)时,max变量的取值是10,而不是100,因为要去创建这个函数的作用域取值,而不是“父作用域”。
当一个函数被调用完成之后,其执行上下文环境将被销毁,其中的变量也会被同时销毁。但有些情况下,函数调用完成之后,其执行上下文环境不会接着被销毁。这就是需要理解闭包的核心内容。如下:
第一步,代码执行前生成全局上下文环境,并在执行时对其中的变量进行赋值。此时全局上下文环境是活动状态。
第二步,执行第17行代码时,调用fn(),产生fn()执行上下文环境,压栈,并设置为活动状态。
第三步,执行完第17行,fn()调用完成。按理说应该销毁掉fn()的执行上下文环境,但是这里不能这么做。注意,重点来了:因为执行fn()时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量max要引用fn作用域下的fn()上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。
因此,这里的fn()上下文环境不能被销毁,还依然存在与执行上下文栈中。——即,执行到第18行时,全局上下文环境将变为活动状态,但是fn()上下文环境依然会在执行上下文栈中。另外,执行完第18行,全局上下文环境中的max被赋值为100。如下图:
第四步,执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。
执行bar(15)时,max是自由变量,需要向创建bar函数的作用域中查找,找到了max的值为10。这个过程在作用域链一节已经讲过。
这里的重点就在于,创建bar函数是在执行fn()时创建的。fn()早就执行结束了,但是fn()执行上下文环境还存在与栈中,因此bar(15)时,max可以查找到。如果fn()上下文环境销毁了,那么max就找不到了。
这里明显可以看到,使用闭包会增加内容开销!
第五步,执行完20行就是上下文环境的销毁过程。
总结
闭包就是一个函数引用另外一个函数的变量,因为变量被引用着所以不会被回收,因此可以用来封装一个私有变量。这是优点也是缺点,不必要的闭包只会徒增内存消耗!另外使用闭包也要注意变量的值是否符合你的要求,因为他就像一个静态私有变量一样。