本文本着3个目的进行探究
1.闭包是什么
2.为什么要用闭包
3.闭包的应用场景
我的傻瓜式学习笔记,给自己备忘,也希望能帮助到一些人,如有错误欢迎指正。O(∩_∩)O~
为什么要用闭包?
作用域
函数是JavaScript中唯一拥有自身作用域的结构。(JavaScript不支持块级作用域)
意味着定义在函数中的参数和变量在函数外部是不可见的,而在一个函数内部任何位置定义的变量,在该函数内部任何地方都可见。
一个值得注意的例子:
1 2 3 4 5 6 7 8 9 10 11 12
| var foo = function(){ var a = 3,b = 5; var bar = function(){ var b = 7,c = 11; a += b+c; }; bar(); };
|
函数访问全局变量
JavaScript中函数内部可以直接访问全局变量。
1 2 3 4 5 6 7
| var n=999; function f1(){ console.log(n); } f1();
|
函数外部无法读取函数内的局部变量。
1 2 3 4 5 6 7 8
| var m=123; function f1(){ var n=999; } console.log(m); console.log(n);
|
1 2 3 4 5 6 7 8 9
| var m=123; function f1(){ var n=999; } f1(); console.log(m); console.log(n);
|
注意:此时的n,如果声明时没有用var命令,在执行了声明它的函数后,事实上是产生了一个全局变量!
1 2 3 4 5 6 7 8
| var m=123; function f1(){ n=999; } console.log(m); console.log(n);
|
1 2 3 4 5 6 7 8 9
| var m=123; function f1(){ n=999; } f1(); console.log(m); console.log(n);
|
1 2 3 4 5 6 7 8
| var n=123; function f1(){ n=999; } f1(); console.log(n);
|
外部访问函数局部变量
正常情况下JavaScript无法获得函数内部的局部变量的值,可是我们有时需要获取,我们应该怎样做?
1 2 3 4 5 6 7 8 9
| function counter(){ var count=5; function get(){ console.log(count); } } counter();
|
以上代码木有反应,因为我们执行了counter函数却没执行counter函数里面的个体函数哟..
我们在counter里面调用一下get();这样执行counter函数即执行了get函数。
1 2 3 4 5 6 7 8 9 10
| function counter(){ var count=5; function get(){ console.log(count); } get(); } counter();
|
这样我们便访问到了counter的局部变量,哈哈,大功告成!…了吗?
我们这样做和一下代码有什么区别?!
1 2 3 4 5 6 7
| function counter(){ var count=5; console.log(count); } counter();
|
这样做行不通,我们另寻他径。
1 2 3 4 5 6 7 8 9 10 11
| function counter(){ var count=5; function get(){ return count; } return get; } var foo=counter(); console.log(foo());
|
我们返回了一个函数名get,当执行了counter函数后返回了counter函数内的get函数的引用并赋值给foo,在外面执行了foo()相当于调用了get(),我们获取到了局部变量count!!!~
以下例子更加灵活
1 2 3 4 5 6 7 8 9 10 11 12
| function counter(){ var count=5; return { get:function(){ return count; } } } var foo=counter(); console.log(foo.get());
|
这次我们return了一个对象字面量,在执行counter函数后返回了一个对象字面量并赋给一个foo变量,里面有个名为get的key,其value为一个匿名函数(是否是匿名并没有关系)。我们执行字面量中的get方法,得到了count。
我们希望能在一个函数中访问局部变量,完成更多的事,不仅仅是这一个用途而已。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function counter(){ var count=5; return { get:function(){ return count; }, increment:function(){ count++; } } } var foo=counter(); console.log(foo.get()); foo.increment(); console.log(foo.get());
|
以上例子也可以这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function counter(){ var count=5; function get(){ return count; } function increment(){ count++; } return { get:get, increment:increment } } var foo=counter(); console.log(foo.get()); foo.increment(); console.log(foo.get());
|
再改进一下,使他更有意义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function counter(start){ var count=start; function get(){ return count; } function increment(){ count++; } return { get:get, increment:increment } } var foo=counter(4); console.log(foo.get()); foo.increment(); console.log(foo.get());
|
至此,我们已经应用到了闭包。
这个例子中的counter函数返回两个闭包,函数get和函数increment。这两个函数都维持着对外部作用域 counter 的引用,因此总可以访问此作用域内定义的变量 count。
以上例子,你还可以这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| var foo = (function (){ var count=0; function setCount(start){ count=start; } function getCount(){ return count; } function increment(){ count++; } return { setCount:setCount, getCount:getCount, increment:increment } }()); console.log(foo.getCount()); foo.increment(); console.log(foo.getCount()); foo.setCount(4); console.log(foo.getCount()); foo.increment(); console.log(foo.getCount());
|
此处我们直接把调用函数后的结果赋值给foo,注意最后一行的(),成为立即执行函数。
之后我们可以直接使用foo.xx的方式来访问相关函数。
这也是JavaScript模块化编程的一种写法。
关于立即执行函数,以下两种写法均可,看个人习惯:
1 2 3 4
| (function () { } ()); (function () { })();
|
(function(){}())是使用了强制运算符执行函数调用运算,(function(){})()是通过函数调用运算符操作函数引用。两者功能上是一致的,只是运算过程不同。
闭包是什么?
闭包是 JavaScript一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为函数是JavaScript中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。
闭包在书中是这样被描述的:
闭包是函数和执行它的作用域组成的综合体 — 《JavaScript权威指南》
闭包是一种在函数内访问和操作外部变量的方式;所有的函数都是闭包;函数可以访问它被创建时的上下文环境,成为闭包 — 《JavaScript语言精粹》
内部函数比它的外部函数具有更长的生命周期
更简单的定义 — 闭包就是能够读取其他函数内部变量的函数。
由于在JavaScript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
改变私有变量?
我们能否在闭包外面改变私有变量呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function counter(start){ var count=start; function get(){ return count; } function increment(){ count++; } return { get:get, increment:increment } } var foo=counter(4); foo.hack = function(){ count = 999; }; console.log(foo.get());
|
这种做法,答案是NO。foo.hack没有定义在counter作用域内,以上代码不会改变定义在counter作用域中的count值。
闭包的优点
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
循环中的闭包
一个常见的错误,出现在循环中使用闭包,假设我们需要在每次循环中访问循环序号
例一:
1 2 3 4 5
| for(var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 1000); }
|
执行结果为:
上面的代码不会输出数字 0 到 9,而是会输出数字 10 十次。
当 console.log 被调用的时候,匿名函数保持对外部变量 i 的引用,此时 for循环已经结束, i 的值被修改成了 10.
为了得到想要的结果,需要在每次循环中创建变量 i 的拷贝。
避免引用错误
为了正确的获得循环序号,最好使用匿名包裹器。
1 2 3 4 5 6 7
| for(var i = 0; i < 10; i++) { (function(e) { setTimeout(function() { console.log(e); }, 1000); })(i); }
|
执行结果为:
外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。
当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。
有另一个方法完成同样的工作;那就是从匿名包装器中返回一个函数。这和上面的代码效果一样。
1 2 3 4 5 6 7
| for(var i = 0; i < 10; i++) { setTimeout((function(e) { return function() { console.log(e); } })(i), 1000) }
|
执行结果为:
例二:
糟糕的例子:
1 2 3 4 5 6 7 8
| var foo=function(nodes){ var i; for(i=0; i<nodes.length; i+=1){ node[i].onclick=function(e){ console.log(i); }; } };
|
错误来源于在循环中创建函数,其本意是想传递给每个事件处理器一个唯一值(i)。但它未能达到目的,因为事件处理器函数绑定了变量i本身,而不是
函数在构造时的变量i的值。
避免引用错误
例一的解法:
1 2 3 4 5 6 7 8 9 10
| var foo=function(nodes){ var i; for(i=0; i<nodes.length; i+=1){ (function(n){ node[n].onclick=function(e){ console.log(n); }; })(i); } };
|
或者你可以这样做,创建一个辅助函数:
1 2 3 4 5 6 7 8 9 10 11
| var foo=function(nodes){ var helper=function(i){ return function(e){ console.log(i); }; }; var i; for(i=0; i<nodes.length; i+=1){ node[i].onclick=helper(i); } };
|
避免在循环中创建函数,在循环中创建函数,会导致错误,只会带来无谓的计算及引起混淆。
在循环之外创建一个辅助函数,让这个辅助函数再返回一个绑定了当前i值的函数,这样就不会导致混淆了。
参考文献:
[1] 学习Javascript闭包(Closure)
[2] JavaScript - The Good Parts
[3] JavaScript 秘密花园 - 闭包和引用
[4] 潜力无限的编程语言JavaScript