浅谈js之闭包
1.什么是闭包???
"官方"的解释是指一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分;
红皮书是这样说的,闭包是指有权访问另一个函数作用域中变量的函数;常见的创建闭包的方式就是在一个函数中再创建一个函数;
闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成;
光看定义是云里雾里,但是到了真正的代码了又是什么样的形式呢?经典的闭包例子:
function fn() { var name = 4; return function () { var n = 0; alert(++n); alert(++name); } } var fun = fn(); fun();//n =>1,name=>5; fun();// n =>1,name=>6;
这里就有闭包产生了,fun就是闭包;这个闭包由fn中的匿名函数和name变量构成。,但是呢,它又是一种特殊的函数,它是一种能能够读取其他函数内部变量的特殊函数;fn中的匿名函数的父函数在执行完了之后,按正常来说,它应该被销毁,里面的变量也被销毁,,但是里面的变量现在没有被销毁,而是被这个匿名函数引用着;(说实在的它不应该被销毁,因为这个匿名函数还没有执行,还要用上一级的函数的中的变量,你给我销毁了,我可怎么办,那不是让我报错呀,但是呢,我给你用,不销毁,这又不符合规矩,按规矩是这样的:当一个函数执行完之后,是要被立即销毁的,执行环境销毁,里面的变量销毁,你现在不让我销毁,那不乱套了,那怎么办呢,于是乎,一群砖家,就说赶这种方式叫闭包吧)意思是说我跟你们不一样;因为是特殊函数,代码的最后一句fun()执行完之后,name变量还是没有释放,但是每次执行fun,里面的n变量是都是新创建的,执行完之后又释放掉;要是你明白了就不需要看括号里的内容了(这里我就形象的说为什么说name变量会一直在内存中?在刚开始的时候,父函数fn在刚要执行完了,开始销毁时,匿名子函数就说了,我要用你的name变量,你先别销毁了,父函数说好吧,于是乎,父函数执行完之后(归天了),就没有销毁name,在当你调用fun,执行匿名子函数,fun()调用完了,你把自己家的n变量销毁了,fun就说了name又不是我的东西,我就是用了一下,凭什么我给销毁,我不给销毁,但是这时父函数已经去世了(执行完了),于是就产生了内存消耗,除非你手动销毁,垃圾收回机制不会自动收回;这又牵扯到内存泄漏,性能问题了,后面说。)
作用域链:
讲到这里,如果要想整整的明白还有知道作用域链,和垃圾收回机制;
我就说说上面代码执行时的作用域链:
我就说说这张图是什么意思,这种图是执行var fun = fu();fun();这两句代码时所发生的情况;
其实匿名函数在fu()被返回时,它的作用域链就被初始化为包含全局变量对象和fu函数的活动对象;也就是说当fu函数执行完返回后,它的执行环境会被销毁,但是其活动对象不会被销毁,仍然在内存中,因为匿名函数的作用域链中引用了这个活动对象。只有到匿名函数被手动销毁时才销毁;其实在fu执行完后,红字显示的部分就消失了,就活动变量没有消失;
再说一点关于作用域链的问题:
1。作用域链中的变量对象(函数中叫活动对象)保存的是变量和函数;
2.作用域链的作用就是为了保证对执行环境有权访问的所有变量和函数的有序访问。
3.查找一个变量是从作用域链的最前端,逐渐向上找,只要找到就不再向上找了,不论上面是否还有这个值;
4.就以上面的fu函数为例吧,在声明fu函数的时候就开始预先创建一个包含全局变量对象的作用域链了(如果嵌套多了,其实就是在函数声明的地方创建父函数及其之上的作用域链),这个作用域链将被保存在刚创建函数的内部[[Scope]]的属性中;当调用fu函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]中的作用域链构建起执行环境的作用域链;之后还要创建一个本函数的活动对象,并把这个活动对象推入执行环境作用域链的前端。
5.作用域链本质上就是一个指向变量对象的指针列表。
垃圾回收机制:
再说说垃圾回收机制:
1.如果一个对象不再有引用了,这个对象就会被GC收回;
2,如果两个对象互相引用,但不被第三个引用,这两个互相引用的对象也会收回的。
而闭包不再这个范畴之内。
闭包的特性:
1.引用的变量不被垃圾回收机制收回
2.函数内部可以引用外部的变量;
3.函数里面嵌套函数
闭包的用处(好处):
1.私有变量和方法
var a=(function() { var privateNum = 1; function privateFun(val) { alert(val); } return { publicFun: function() { privateFun(2); }, publicNum:function() { return privateNum; } } })(); a.publicNum();//1 a.publicFun();//2
如果你用a.privateNum,a.privateFun();这是会报错的。
2.实现一些变量的累加
function a() { var n=0; return function () { n++; alert(n); } } var b = a(); b();//1 b();//2 b();//3
这里只是要使用累加,就这样干,具体还要具体分析,原理是这样了
因闭包产生的问题
初学者常见的,循环闭包
大部分我们所写的 Web JavaScript 代码都是事件驱动的 — 定义某种行为,然后将其添加到用户触发的事件之上(比如点击或者按键)。我们的代码通常添加为回调:响应事件而执行的函数。
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"/> <title>闭包循环问题</title> <style type="text/css"> p {background:red;} </style> </head> <body> <p class="p">我是1号</p> <p class="p">我是2号</p> <p class="p">我是3号</p> <p class="p">我是4号</p> <p class="p">我是五号</p> <script type="text/javascript"> var page = document.getElementsByTagName("p"); for(var i=0; i<page.length; i++) { page[i].onclick = function () { alert("我是"+i+"号"); } } </script> </body> </html>
你不管点击哪一个,都alert”我是5号“;
原因就是你循环了五次产生了五个闭包,而这5个闭包共享一个变量i,说的明白一点就是,在for循环结束时,只是把这五个匿名函数注册给click事件,当时在循环的时候并没有执行,当循环结束了,此时i的值是5;之后你去点击p标签,你点击哪一个就执行哪一个对应的匿名函数(这个时候才执行),这时候匿名中发现一个i,匿名中没有定义i,于是沿着作用域链找,找到了,但是这时候循环早就结束了,i等于5,于是弹出”我是5号“来;点击其他的同理;
怎么解决呢:
一种方法是再创建一个闭包,把js代码改为这样就行了
var page = document.getElementsByTagName("p"); for(var i=0; i<page.length; i++) { !function(num) { page[i].onclick = function () { alert("我是"+num+"号"); } }(i) }
我只说一点,这次五个闭包不共享num,而是创建五个num变量
还有一种解决方式:
var page = document.getElementsByTagName("p"); for(var i=0; i<page.length; i++) { page[i].num = i//先把每个变量值存起来 page[i].onclick = function () { alert("我是"+this.num+"号"); } }
闭包中的this对象
var num = 1; var obj = { num:2, getNum:function() { return function () { return this.num; } } } alert(obj.getNum()());//num -> 1
为什么不弹出2呢,这里是说明闭包中你需要注意现在的this的指向那一个对象,其实记住一句话就永远不会用错this的指向问题,this永远指向调用它的作用域;
如果这样写你就可能理解了
var num = 1; var obj = { num:2, getNum:function() { return function () { return this.num; } } } var a = obj.getNum(); alert(window.a());//1
其实是window对象调用的,这就是说闭包中的this让你看不清this的指向;
要是让它alert 2你要这样:
var num = 1; var obj = { num:2, getNum:function() { var _this = this;//在这里保存this return function () { return _this.num; } } } var a = obj.getNum(); alert(window.a());
性能考量
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次(也就是说,为每一个对象的创建)。
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
应该是这样
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
示例中,继承的原型可以为所有对象共享,且不必在每一次创建对象时定义方法
欢迎评论指正;
参考:
红皮书
Mozilla:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Closures
阮一峰:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html