1 变量的作用域
2变量的生存周期
对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。
1 <html>
2 <body>
3 <div>1</div>
4 <div>2</div>
5 <div>3</div>
6 <div>4</div>
7 <div>5</div>
8 <script>
9 var nodes = document.getElementsByTagName( 'div' );
10 for ( var i = 0, len = nodes.length; i < len; i++ ){
11 nodes[ i ].onclick = function(){
12 alert ( i );
13 }
14 };
15 </script>
16 </body>
17 </html>
测试这段代码就会发现,无论点击哪个 div ,最后弹出的结果都是 5。这是因为 div 节点的onclick 事件是被异步触发的,当事件被触发的时候, for 循环早已结束,此时变量 i 的值已经是5,所以在 div 的 onclick 事件函数中顺着作用域链从内到外查找变量 i 时,查找到的值总是 5。
解决方法是在闭包的帮助下,把每次循环的 i 值都封闭起来。当在事件函数中顺着作用域链中从内到外查找变量 i 时,会先找到被封闭在闭包环境中的 i ,如果有 5个 div ,这里的 i 就分别是 0,1,2,3,4:
1 var Type = {};
2 for ( var i = 0, type; type = [ 'String', 'Array', 'Number' ][ i++ ]; ){
3 (function( type ){
4 Type[ 'is' + type ] = function( obj ){
5 return Object.prototype.toString.call( obj ) === '[object '+ type +']';
6 }
7 })( type )
8 };
9 Type.isArray( [] ); // 输出:true
10 Type.isString( "str" ); // 输出:true
.3 闭包的更多作用
1) 封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。
1 //计算乘积的简单函数:
2 var mult = function(){
3 var a = 1;
4 for ( var i = 0, l = arguments.length; i < l; i++ ){
5 a = a * arguments[i];
6 }
7 return a;
8 };
mult 函数接受一些 number 类型的参数,并返回这些参数的乘积。现在我们觉得对于那些相同的参数来说,每次都进行计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能:
1 var cache = {};
2 var mult = function(){
3 var args = Array.prototype.join.call( arguments, ',' );
4 if ( cache[ args ] ){
5 return cache[ args ];
6 }
7 var a = 1;
8 for ( var i = 0, l = arguments.length; i < l; i++ ){
9 a = a * arguments[i];
10 }
11 return cache[ args ] = a;
12 };
13 alert ( mult( 1,2,3 ) ); // 输出:6
14 alert ( mult( 1,2,3 ) ); // 输出:6
我们看到 cache 这个变量仅仅在 mult 函数中被使用,与其让 cache 变量跟 mult 函数一起平行地暴露在全局作用域下,不如把它封闭在 mult 函数内部,这样可以减少页面中的全局变量,以避免这个变量在其他地方被不小心修改而引发错误。代码如下:
1 var mult = (function(){
2 var cache = {};
3 return function(){
4 var args = Array.prototype.join.call( arguments, ',' );
5 if ( args in cache ){
6 return cache[ args ];
7 }
8 var a = 1;
9 for ( var i = 0, l = arguments.length; i < l; i++ ){
10 a = a * arguments[i];
11 }
12 return cache[ args ] = a;
13 }
14 })();
提炼函数是代码重构中的一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,我们常常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码复用,如果这些小函数有一个良好的命名,它们本身也起到了注释的作用。如果这些小函数不需要在程序的其他地方使用,最好是把它们用闭包封闭起来。代码如下:
1 var mult = (function(){
2 var cache = {};
3 var calculate = function(){ // 封闭 calculate 函数
4 var a = 1;
5 for ( var i = 0, l = arguments.length; i < l; i++ ){
6 a = a * arguments[i];
7 }
8 return a;
9 };
10 return function(){
11 var args = Array.prototype.join.call( arguments, ',' );
12 if ( args in cache ){
13 return cache[ args ];
14 }
15 return cache[ args ] = calculate.apply( null, arguments );
16 }
17 })();
2)延续局部变量的寿命
img 对象经常用于进行数据上报,如下所示:
1 var report = function( src ){
2 var img = new Image();
3 img.src = src;
4 };
5 report( 'http://xxx.com/getUserInfo' );
report 函数并不是每一次都成功发起了 HTTP请求。丢失数据的原因是 img 是 report 函数中的局部变量,当 report 函数的调用结束后, img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求就会丢失掉。
现在我们把 img 变量用闭包封闭起来,便能解决请求丢失的问题:
1 var report = (function(){
2 var imgs = [];
3 return function( src ){
4 var img = new Image();
5 imgs.push( img );
6 img.src = src;
7 }
8 })();
4) 闭包和面向对象设计
使用闭包来实现一个完整的面向对象系统。
1 var extent = function(){
2 var value = 0;
3 return {
4 call: function(){
5 value++;
6 console.log( value );
7 }
8 }
9 };
10 var extent = extent();
11 extent.call(); // 输出:1
12 extent.call(); // 输出:2
13 extent.call(); // 输出:3
如果换成面向对象的写法,就是:
1 var extent = {
2 value: 0,
3 call: function(){
4 this.value++;
5 console.log( this.value );
6 }
7 };
8 extent.call(); // 输出:1
9 extent.call(); // 输出:2
10 extent.call(); // 输出:3
或
1 var Extent = function(){
2 this.value = 0;
3 };
4 Extent.prototype.call = function(){
5 this.value++;
6 console.log( this.value );
7 };
8 var extent = new Extent();
9 extent.call();
10 extent.call();
11 extent.call();
5)用闭包实现命令模式
1 <html>
2 <body>
3 <button id="execute">点击我执行命令</button>
4 <button id="undo">点击我执行命令</button>
5 <script>
6 var Tv = {
7 open: function(){
8 console.log( '打开电视机' );
9 },
10 close: function(){
11 console.log( '关上电视机' );
12 }
13 };
14 var OpenTvCommand = function( receiver ){
15 this.receiver = receiver;
16 };
17 OpenTvCommand.prototype.execute = function(){
18 this.receiver.open(); // 执行命令,打开电视机
19 };
20 OpenTvCommand.prototype.undo = function(){
21 this.receiver.close(); // 撤销命令,关闭电视机
22 };
23 var setCommand = function( command ){
24 document.getElementById( 'execute' ).onclick = function(){
25 command.execute(); // 输出:打开电视机
26 }
27 document.getElementById( 'undo' ).onclick = function(){
28 command.undo(); // 输出:关闭电视机
29 }
30 };
31 setCommand( new OpenTvCommand( Tv ) );
32 </script>
33 </body>
34 </html>
命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接收者。但在 JavaScript中,函数作为一等对象,本身就可以四处传递,用函数对象而不是普通对象来封装请求显得更加简单和自然。如果需要往函数对象中预先植入命令的接收者,那么闭包可以完成这个工作。在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起来;而在闭包版本的命令模式中,命令接收者会被封闭在闭包形成的环境中,代码如下:
1 var Tv = {
2 open: function(){
3 console.log( '打开电视机' );
4 },
5 close: function(){
6 console.log( '关上电视机' );
7 }
8 };
9 var createCommand = function( receiver ){
10 var execute = function(){
11 return receiver.open(); // 执行命令,打开电视机
12 }
13 var undo = function(){
14 return receiver.close(); // 执行命令,关闭电视机
15 }
16 return {
17 execute: execute,
18 undo: undo
19 }
20 };
21 var setCommand = function( command ){
22 document.getElementById( 'execute' ).onclick = function(){
23 command.execute(); // 输出:打开电视机
24 }
25 document.getElementById( 'undo' ).onclick = function(){
26 command.undo(); // 输出:关闭电视机
27 }
28 };
29 setCommand( createCommand( Tv ) );
6)闭包与内存管理
闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用。
局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。从这个意义上看,闭包的确会使一些数据无法被及时销毁。使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,这里并不能说成是内存泄露。如果在将来需要回收这些变量,我们可以手动把这些变量设为 null 。
跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些 DOM节点,这时候就有可能造成内存泄露。但这本身并非闭包的问题,也并非 JavaScript的问题。在 IE 浏览器中,由于 BOM 和 DOM 中的对象是使用 C++以 COM 对象的方式实现的,而 COM对象的垃圾收集机制采用的是引用计数策略。在基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄露在本质上也不是闭包造成的。
同样,如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为 null即可。将变量设置为 null 意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
----《JavaScript设计模式与开发实践》读书笔记
浙公网安备 33010602011771号