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设计模式与开发实践》读书笔记

posted on 2018-07-11 15:15  小小慧house  阅读(97)  评论(0)    收藏  举报