闭包这东西,说难也难,说不难也不难,下面我就以自己的理解来说一下闭包。
一、闭包的解释说明
对于函数式语言来说,函数可以保存内部的数据状态。对于像C#这种编译型命令式语言来说,由于代码总是在代码段中执行,而代码段是只读的,因此函数中的数据只能是静态数据。函数内部的局部变量存放在栈上,在函数执行结束以后,所占用的栈被释放,因此局部变量是不能保存的。
Javascript采用词法作用域,函数的执行依赖于变量作用域,这个作用域是在定义函数时确定的。因此Javascript中函数对象不仅保存代码逻辑,还必须引用当前的作用域链。Javascript中函数内部的局部变量可以被修改,而且当再次进入到函数内部的时候,上次被修改的状态仍然持续。这是因为因为局部变量并不保存在栈上,而是通过一个对象来保存。
决定使用哪个变量是查找作用域链后决定的。
1.每次定义函数时,都会为之创建一个作用域链;
2.每次调用函数时,会创建一个新的对象用来保存变量(局部变量和参数),并且把这个用于保存变量的对象加入作用域链前端。
如果没有局部变量的话,这个保存变量的对象就只包含arguments,也就是用来保存参数。设想这么一种情况,如果内层函数定义时恰逢外层函数正被调用执行,那么内层函数就会把外层函数运行时这个保存变量的对象纳入自己的作用域链中。
用代码来举例,外层匿名函数执行了10次,产生了10个不同的变量保存对象,分别保存了i从0~9的值。如果没有正在执行的外层函数,而是直接定义返回内层函数,那么10个内层函数会共享一个保存变量的i。我们之所以在外层写了立即执行的匿名函数,是利用了函数执行时会产生变量保存对象这一性质。
for (var i=0; i < 10; i++){ result[i] = function(num){ //外层在执行 return function(){ //内层在定义 return num; }; }(i); }
作用域链本质上是一个指向变量对象的指针列表,不同函数对象可以通过作用域链关联起来。Javascript中所有函数都包含了作用域链和代码块,所以Javascript中所有函数从技术上来说都是闭包,我们不能避免“产生”闭包。
引用一张《Javascript高级程序设计》中的图来说明,虽然这张图并不完全说明所有情况。图中的activation object就是用于保存局部变量的对象。
简而言之,在Javascript中:
闭包:函数实例保存着在执行时所需要的变量的引用,而不会复制保存当时变量的值。(在Object C的实现中,我们可以选择保存当时的值或者是引用)
作用域链:解析变量时查找变量所在的方式,以var作为终止符号,如果链上一直没有var,则一直追溯到全局对象为止。
C#中的闭包特性是由编译器把局部变量转换成引用类型的对象成员实现的。
二、闭包的产生
下面通过一些具体例子来说明如何利用闭包这一特性:
1.闭包是在定义的时候产生的
function Foo(){ function A(){} function B(){} function C(){} }
我们每次执行Foo()的时候,都有有A,B,C这三个函数实例(闭包)产生,当Foo执行完毕,生成的实例没有其他引用,因此会被当成垃圾随之销毁(不一定是马上销毁)。
我们来证实一下作用域链是在函数定义时确定的,所以这里显示的应该是'local scope'
var scope = "global scope"; function checkscope() { var scope = "local scope"; function f() { return scope; } return f; } checkscope()()
为了加深印象,对比下面两个例子,
写法一:用了预先定义的函数实例returnNumber
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = returnNumber; } return result; } function returnNumber(i){ return i; }
写法二:用了匿名函数,
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ return i; }; } return result; }
根据我们前面的结论:
1.匿名函数是在循环里执行时定义的,那么它的作用域链就是在循环里创建的,作用域链会包含for执行时产生的保存变量的对象。
2.函数returnNumber是在解析的时候定义的,作用域链就是在全局闭包里创建的,因此i应该是全局对象的属性i.
我们这么写测试代码,看看是否有区别:
var funcs = createFunctions(); for (var i = funcs.length - 1; i >= 0; i--) { alert(funcs[i]()); }
我们竟然发现两种不同写法都好像没有区别。这是为什么呢?第一种写法i的值应该是undefined才对。
检查一下,问题就是在javascript中,for是没有单独的作用域块的,因此测试代码中i是定义在了全局对象上,所以凑巧就被加入returnNumber的作用域链中,所以i这时i就有值了。
我们把测试代码修改了一下,
var funcs = createFunctions(); (function(){ for (var i = funcs.length - 1; i >= 0; i--) { alert(funcs[i]()); } })();
好,这时就符合我们的预期,写法一出现了i is undefined的错误。
所以记住,作用域链是在定义函数的时候创建的。
同样道理:
(function(){ function A(){} function B(){} function C(){} }())
上面的表达式执行完后也会有A,B,C这三个函数实例(闭包)产生,因为这是一个立即执行的匿名函数,这三个闭包只能产生一次。生成的闭包没有其他引用,因此会被当成垃圾随之销毁(不一定是马上销毁)。
我们之所以这么写,目地有两个
1.避免污染全局对象
2.避免多次产生相同的函数实例
对比下面两个例子,闭包是如何保存作用域链的:
function A(){} //比较省内存的写法,创建对象速度快,开销小 (function(prototype){ var name = "a"; function sayName () { alert(name); } function ChangeName() { name += "_changed" } prototype.sayName = sayName;//引用通过执行匿名函数产生的闭包,闭包只会产生一次 prototype.changeName = ChangeName; }(A.prototype)) var a1 = new A(); var a2 = new A(); a1.sayName(); a1.changeName(); a2.sayName();
function B(){ //原型链比较短的做法,找到方法的速度快,但是比较耗内存,每次new 调用构造器都有2个函数实例和1个变量产生。 var name = "b"; function sayName () { alert(name); } function changeName() { name += "_changed"; } this.sayName = sayName;//引用闭包,每次调用函数B都会产生新的闭包 this.changeName = changeName; } //如果函数调用之前带有new关键字,则函数作为构造器使用。 //本质上来说作为构造器和作为普通函数调用没区别。如果直接调用B(),那么this对象会绑定到全局对象,新生成的闭包会代替旧的闭包赋给全局对象的changeName和sayName属性上,因此旧的闭包会被当成垃圾回收。 //如果作为构造器使用,new 关键字会生成一个新的对象(this指向这个新对象)并初始化这个新对象的sayName和changeName属性,因此每次生成的闭包都会因为有引用而保留下来。 var b1 = new B(); b1.sayName(); b1.changeName(); b1.sayName(); var b2 = new B(); b2.sayName(); b1.sayName();
三、闭包的使用
如前面所述,函数在调用时,会产生用于保存局部变量的对象(active object),并加入作用域链中,那么会有如下问题
function createFunctions(){ //(闭包1) var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ //(闭包2), 生成了10份。闭包2生成时会把闭包1保存变量的对象加入到自己的作用域链中。定义函数的时候生成作用域链。 return i; //i并不是定义在(闭包2)中的局部变量或者形参,因此不会被加入到(闭包2)的保存变量的对象中。i来自(闭包1),(闭包1)只有一份,因此10个(闭包2)共享1个变量i. } }; return result; }
修改法1:在外加一层闭包,给一个形参
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(num){ return function(){ return num; }; }(i); } return result; }
function createFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function () { var num = i; return function () { return num; }; }(); } return result; }
function createFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { (result[i] = function () { return argument.callee.num; }).num = i; } return result; }
四、泄漏问题:
在编译语言中,函数体总在文件的代码段中,并在运行期被装入标志为可执行的内存区。事实上我们不认为函数自身会有生命周期。我们在大多数情况下会认为“引用类型的数据结构”具有生存周期和泄漏的问题,如指针、对象等。
JavaScript中内存的泄漏本质上就是调用函数时生成的保存局部变量的对象因为存在引用而不被当成垃圾被回收。
1.存在循环引用
2.有些对象总不能销毁,如IE在DOM中的内存泄漏,或者在销毁时不能通知到Javascript引擎,因此也就有些Javascript闭包总不能被销毁。这些情况通常是发生在Javascript宿主对象和Javascript中原生对象或方法沟通不畅导致。比如内存中留有那些过时不用的“空事件处理程序”(dangling event handler),也是造成Web 应用程序内存与性能问题的主要原因。
在两种情况下,可能会造成上述问题。第一种情况就是从文档中移除带有事件处理程序的元素时。 这可能是通过纯粹的 DOM 操作,例如使用 removeChild()和replaceChild()方法,但更多地是发生在使用 innerHTML 替换页面中某一部分的时候。如果带有事件处理程序的元素被 innerHTML 删除 了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。来看下面的例子。
<div id="myDiv"> <input type="button" value="Click Me" id="myBtn"> </div> <script type="text/javascript"> var btn = document.getElementById("myBtn"); btn.onclick = function(){ //先执行某些操作 document.getElementById("myDiv").innerHTML = "Processing..."; //出问题了! }; </script>
这里,有一个按钮被包含在<div>元素中。为避免双击,单击这个按钮时就将按钮移除并替换成一 条消息;这是网站设计中非常流行的一种做法。但问题在于,当按钮被从页面中移除时,它还带着一个 事件处理程序呢。在<div>元素上设置 innerHTML 可以把按钮移走,但事件处理程序仍然与按钮保持着引用关系。有的浏览器(尤其是 IE)在这种情况下不会作出恰当地处理,它们很有可能会将对元素和对事件处理程序的引用都保存在内存中。如果你知道某个元素即将被移除,那么最好手工把事件处理程序设为Null .
导致“空事件处理程序”的另一种情况,就是卸载页面的时候。毫不奇怪,IE8 及更早版本在这种 情况下依然是问题最多的浏览器,尽管其他浏览器或多或少也有类似的问题。如果在页面被卸载之前没 有清理干净事件处理程序,那它们就会滞留在内存中。每次加载完页面再卸载页面时(可能是在两个页面间来回切换,也可以是单击了“刷新”按钮),内存中滞留的对象数目就会增加,因为事件处理程序占用的内存并没有被释放。
五、分析一段写得不错的代码,作为我们上面内容的总结。
var DragDrop = function(){ var dragdrop = new EventTarget(), dragging = null, diffX = 0, diffY = 0; function handleEvent(event){ //get event and target event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); //determine the type of event switch(event.type){ case "mousedown": if (target.className.indexOf("draggable") > -1){ dragging = target; diffX = event.clientX - target.offsetLeft; diffY = event.clientY - target.offsetTop; dragdrop.fire({type:"dragstart", target: dragging, x: event.clientX, y: event.clientY}); } break; case "mousemove": if (dragging !== null){ //assign location dragging.style.left = (event.clientX - diffX) + "px"; dragging.style.top = (event.clientY - diffY) + "px"; //fire custom event dragdrop.fire({type:"drag", target: dragging, x: event.clientX, y: event.clientY}); } break; case "mouseup": dragdrop.fire({type:"dragend", target: dragging, x: event.clientX, y: event.clientY}); dragging = null; break; } }; //public interface dragdrop.enable = function(){ EventUtil.addHandler(document, "mousedown", handleEvent); EventUtil.addHandler(document, "mousemove", handleEvent); EventUtil.addHandler(document, "mouseup", handleEvent); }; dragdrop.disable = function(){ EventUtil.removeHandler(document, "mousedown", handleEvent); EventUtil.removeHandler(document, "mousemove", handleEvent); EventUtil.removeHandler(document, "mouseup", handleEvent); }; return dragdrop; }();
1.这段代码里,在一段立即执行的函数内,一共只产生了三个函数实例,分别是handleEvent和两个匿名函数。此外,对不同事件mousemove,mousemove,mouseup使用同一个一个函数实例handleEvent作为事件处理器,做到了创建尽量少的函数实例,这样内存占用就小。
2.合理的使用闭包和委托模式使得代码模块化,闭包handleEvent公开的方式是注册到了document对象上,事件的回调处理则委托给了dragdrop对象。