之前面试被问及神马是闭包,作为一只刚出生的菜鸟表示当时懵逼
(注:为了解释方便,以下代码来自MDN,不懂的直接去MDN:https://developer.mozilla.org/cn/docs/Web/JavaScript/Closures)
说到闭包不得不说说js变量作用域。js中变量的作用域属于链式的,就是说,任何函数都可以访问祖先(包括父亲、爷爷等等)函数中的变量。如:
1 function init() { 2 var name = "Mozilla"; 3 function displayName() { 4 alert(name); 5 } 6 displayName(); 7 } 8 init();
以上结果是 Mozilla。
子函数 displayName 访问的是父函数的变量name,那有人会问,那如果init函数还有个父函数,且该父函数也有个name变量,那displayName函数到底访问哪个?相信大家猜到答案了,既然是链式作用域,那凭感觉应该是访问最近的祖先函数定义的变量啦,不信可以用下面代码试试:
1 function initFather(){ 2 var name = "father"; 3 function init() { 4 var name = "Mozilla"; 5 function displayName() { 6 alert(name); 7 } 8 displayName(); 9 } 10 init(); 11 } 12 initFather();
结果是 Mozilla。
讲完基础知识,那接下来开始磨刀霍霍了。
闭包,到底什么是闭包咧。
1 function makeFunc() { 2 var name = "Mozilla"; 3 function displayName() { 4 alert(name); 5 } 6 return displayName; 7 } 8 9 var myFunc = makeFunc(); 10 myFunc();
以上代码还是能输出结果 Mozilla而不是undifined,为什么呢,因为,函数makeFunc中子函数displayName使用了父函数makeFunc的局部变量,但是头疼的是,这个子函数被作为返回值赋值给全局变量了。那么问题来了,一般情况下,函数中的局部变量在函数执行的时候存在,执行结束之后就销毁。可是在这种情况下,变量name是去还是留呢?
由于以上情况在网页开发中经常出现且非常有用,所以name选择留下来。那,留下来总得有个理由吧,行!给你个理由,你存在的地方就是个闭包!也就是说定义你的函数就是一个闭包,但是别忘了你得有兄弟函数(在同样的环境中被定义)使用你。当闭包函数执行时,且你被以某种方式返回成为全局变量的一部分,你就拥有你的独立空间,不会随着父函数的结束而结束。
更重要的是!每次调用这个闭包时这里的name都一人一个位置,纯属复制,但是复制之后各走各的,以后互不相干(科学的说法叫做:保存不同的环境)。相当于是克隆体,这个闭包就相当于是个造血干细胞(不喜勿喷)。来来来,不懂还有代码:
1 function makeAdder(x) { 2 return function(y) { 3 return x + y; 4 }; 5 } 6 7 var add5 = makeAdder(5); 8 var add10 = makeAdder(10); 9 10 console.log(add5(2)); // 7 11 console.log(add10(2)); // 12
氮素,这种小儿科的用法成就了闭包?负责任的告诉你,不可能。
现在看到这里,你有没有觉得闭包跟对象有点像啊,对啊,用对象构造器定义的对象也是个闭包。
网页开发里面经常遇到的代码类似这样:
document.getElementById('myId').onclick = doSomething;
如果这种情况遇到循环,很可能有人写成这样:
1 <body> 2 <p id="help">Helpful notes will appear here</p> 3 <p>E-mail: <input type="text" id="email" name="email"></p> 4 <p>Name: <input type="text" id="name" name="name"></p> 5 <p>Age: <input type="text" id="age" name="age"></p> 6 <script> 7 function showHelp(help) { 8 document.getElementById('help').innerHTML = help; 9 } 10 11 function setupHelp() { 12 var helpText = [ 13 {'id': 'email', 'help': 'Your e-mail address'}, 14 {'id': 'name', 'help': 'Your full name'}, 15 {'id': 'age', 'help': 'Your age (you must be over 16)'} 16 ]; 17 18 for (var i = 0; i < helpText.length; i++) { 19 var item = helpText[i]; 20 document.getElementById(item.id).onfocus = function() { 21 showHelp(item.help); 22 } 23 } 24 } 25 26 setupHelp(); 27 </script> 28 </body>
你去试试吧。告诉你为什么不对,有没有注意到,onfocus被赋值的是个匿名函数,它哪一点看起来像闭包了。所以,匿名函数里面的item是几个意思,item是匿名函数之外定义的。所以,既然不是闭包,那就是普通的函数,所有被赋值这个匿名函数的属性(onclick)在实际触发的时候使用的是同一个item对象,此时的item就是循环最后一轮的item。诶,这个时候item还在?为什么不该在?onclick是全局的吧,那,setupHelp函数是闭包吧。
所以,上面那个问题的解决方案很简单,给onfocus赋值一个闭包就行了,
function makeHelpCallback(help) { return function() { document.getElementById('help').innerHTML = help; }; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); } } setupHelp()
结尾再说点完全不是废话的话,刚刚说到,用构造器构造的对象如果同时构造了方法,那么这个对象就是个闭包。建议不要这样做,因为闭包的缘故,闭包中的方法跟闭包中的对象一样,在生成一个对象时是独立创建的,也就是说,每次生成一个对象函数就会被重新创建一次。但是,如果没有其它特殊用法,这样做只会浪费内存。
所以解决办法就是,使用原型链prototype:
1 function MyObject(name, message) { 2 this.name = name.toString(); 3 this.message = message.toString(); 4 } 5 MyObject.prototype.getName = function() { 6 return this.name; 7 }; 8 MyObject.prototype.getMessage = function() { 9 return this.message; 10 };
废话讲完了...