月度归档:2015年03月

浅谈Javascript闭包

垃圾回收器

我个人把闭包抽象的称之为”阻止垃圾回收器的函数”或者”有权访问另一个函数内部变量的函数”(当然这个是我个人的理解方式,每个人可能会有不同的理解方式),为什么这样说?这样说还得说说垃圾回收器,一些编程语言如C语言对于内存管理是由程序员说了算,也就是说程序员决定这个变量是否还需要,如果不需要就释放这个变量占用的空间,而这个变量也不复存在了,这样做的好处是可以及时释放内存空间,让那些需要使用内存空间的程序来使用。

 

而在Javascript中,我们是不需要这样做的,Javascript有一个叫”垃圾回收器”的系统,它会自动对于那些我们不再使用的变量进行回收。那问题来了,它怎么知道我们不再需要这个变量了?在Javascript里面有两个回收机制一个为:”引用计数”,另外一个为”标记清除”,垃圾回收器会根据代码里面对变量的调用(引用)来判断,如果这个变量的调用(引用)为0那么就表示可以回收了。

 

引用计数

引用计数是如何工作?拿一个例子来说吧:

 

“有一家公司的小明,他工作是和销售有关的,所以他每天会开公司的车去见客户、接客户进行推销。而取得公车的步骤需要向管车的王大爷进行预约,然后王大爷会把预约纪录到今天的预约表上,使用完后需要把车交给王大爷并把记录划掉。有一天小明需要使用公车,小明打电话给王大爷表明他下午两点会使用公车,王大爷随后把此次预约纪录到了今天的预约表上,到了约定时间,小明取到了公车。随后与客户一番见面商谈过后把公车交还给王大爷,王大爷同时也把预约表上的小明这一条记录给划掉,王大爷随后看了看预约表,今天没有人预约了,看来可以早点下班了,于是把车停回车库下班了。”

 

其实上面的例子可以看出来,王大爷相当于”垃圾回收器”,而王大爷依据什么来进行处理回收的?是根据预约表,而这个预约表就相当于”引用计数”,当这个预约表上没有人预约公车的时候,王大爷就把车停回了车库,当然如果还有预约王大爷还得一直等待预约表上没有任何预约了才能把车停回车库并下班。也就相当于当这个变量没有人在需要使用的时候,这个变量就可以被“回收”了。

 

标记清除 

这一节点很感谢”linkFly“前辈指出我的错误,我错误的把”引用计数”理解为最常用的计数了,所以在这里感谢”linkFly“前辈。

目前”标记清除”技术是最常用的一种内存回收机制,现在chrome和Safari以及新版本IE、opera等浏览器均采用这样的内存回收机制。

“标记清除”不同于”引用计数”,标记清除更加简单,相当于告诉我们这个变量能使用或者不能用。当一个变量声明的时候,变量会被标记为”进入环境”,而当变量离开了环境过后就标记为”离开环境”。

标记清除的工作方式大概为:

首先javascript会在全局中给所有的变量加上标记,然后去掉在环境中的变量以及环境中被引用的变量的标记,其他被标记的变量表示可以清除的变量,在垃圾回收器执行的时候将会把这些变量清除掉。

 

什么是闭包?

在正常的回收机制下,当我们不再使用这个函数或者变量的时候就会被回收,不再使用是指在当前我们执行的状态之后没有出现在调用(引用)变量的语句了。下面有一个例子,简单的介绍了一下闭包,例子中logHello被赋值一个自执行匿名函数返回的函数,这个函数是有权限访问自执行匿名函数内部的log_num变量的,这样的函数被称为闭包,因为返回的函数内部有一个对自执行匿名函数内部log_num变量的引用。

 

 1 var logHello;
 2 
 3 
 4 logHello = ( function () {
 5 
 6     var log_num = 20;
 7 
 8     return function() {
 9 
10            return log_num;
11 
12     };
13 
14 } )();

 

 

下面还有一个例子,logHello被赋值自执行函数返回的一个对象,这个对象里面包含了两个方法”read”和”write”,这两个方法都属于闭包,他们都有权访问log_name变量。”read”负责读取log_name这个变量的值,而”write”负责重写log_name的值,在外部是没有任何情况直接访问log_name变量的,只能通过这两个方法来进行间接的访问和设置这个变量,这样的方法同样属于闭包。

 1 var logHello;
 2 
 3 logHello = ( function () {
 4     
 5     var log_name = 'wyz';
 6 
 7     return {
 8         read: function () {
 9             return log_name;
10         },
11 
12         write: function (newValue) {
13             log_name = newValue;
14         }
15     };
16 
17 } )();

 

 

闭包是怎么形成的?

从上面可以知道判断一个变量是否可以回收,根据这个变量的是否被引用就可以判断,比如下面例中,局部变量log_name被返回的匿名函数引用,那么就不会被垃圾回收器回收,这就是闭包的形成,因为闭包会保持对变量引用,让垃圾回收器不会回收此变量。

 1 var logHello;
 2 
 3 logHello = ( function () {
 4 
 5 
 6     var log_num = 20;
 7 
 8     return function() {
 9 
10            return log_num;       <<返回一个匿名函数,函数内部可以对局部变量log_num进行访问。
11 
12     };
13 
14 
15 } )();

 

经过上面的一些例子,就很容易理解了,噢~还没有理解?,那你再继续看看后面的总结吧(但相信我,多思考思考,肯定能理解)。

闭包可以理解为:会保存对变量的引用而不会让变量被垃圾回收器回收。

事件捕获与事件冒泡

事件流

在理解这两个概念之前可以先来理解一个概念叫:事件流,指的是触发事件的先后顺序。可以把事件冒泡和事件捕获想成对于事件流的一种实现方式。

很久之前IE和网景公司对于事件实现的时候产生一个方面的共识,那就是当我们点击一个网页的时候,点击页面内任何一个元素,那么点击的不只是那一个元素,而是整个页面。相当于一个同心圆,我们手指指向中心的时候,其实指向的不只是中心圆,而是所有的圆。如下图。但是在关于如何实现事件流的时候,两个公司却给出了截然相反的理念。

 

事件冒泡

这个由微软提出并使用在IE浏览器中,事件冒泡的基本理念是从特定目标到不特定目标的顺序进行触发。那么触发的顺序就如同下图:

 

 

 

如有下例一个页面:

1 
2 
3     
4 
5         
6 
7     
8 
9     

当我们点击页面内的input元素的时候,如果只看这个结构里面的内容,那么触发的顺序为:input >> body >> html

 

事件捕获

这个由网景公司提出,事件捕获的基本理念是从不特定目标到特定目标的顺序进行触发。那么触发的顺序如下图:

 

 

如有下例一个页面:

1 <html>
2 
3     <body>
4 
5         <input type=“submit” />
6 
7     </body>
8 
9 </html>    

 

当我们点击页面内的input元素的时候,如果只看这个结构里面的内容,那么触发的顺序为:html >> body >> input

 

 

DOM2级事件

W3C在很久之前就着手规划事件,把事件分为两个部分,第一个部分为事件捕获阶段,第二个部分为事件冒泡阶段。相当于每个事件经历了两个步骤,一个为事件捕获,一个为事件冒泡,就如下图一样:

 

 

关于兼容问题,从IE9开始的浏览器以及现在的Safari、firefox、chrome、opera浏览器都支持DOM2级事件,但是如果要兼容IE9以下的浏览器,那就需要使用时间冒泡类型来兼容IE9以后的浏览器了。

Call与Apply

1、前言

ECMAscript中提供了两个方法(call,apply)用于改变对象内部的this指针,它们两个的作用都是一样的,但是传递的参数有点不大相同。

它们的大概语法为:

call(this, arg1, arg2, arg3, …..)

apply(this, arguments);

它们第一个参数都是需要改变指针的对象,之后的参数是传递给在调用call方法的函数需要的参数。

call之后的需要传递多少参数就传递多少参数,而apply传递的是一个参数数组,它们两个有什么不一样?call是在明确知道参数有多少个的情况下使用,而apply是相对于不清楚有多少个参数的时候使用的。

 

 

2、它们有什么作用?

比如在很多情况下,操作DOM返回的NodeList类型的值是一个类数组,相当于有数组的基本特征但是没有数组的很多方法,所以这个时候就需要用call方法调用Array数组类型的一些方法。

具体这这两个方法有什么用?还是实例来说明吧!

 1 var a = {
 2 
 3 value: 10
 4 
 5 };
 6 
 7 var b = {
 8 
 9 setValue: function(num){
10 
11 this.value = this.value + num;
12 
13 }
14 
15 }
16 
17 b.setValue.call(a, 20); <<执行过后,a.value等于30;

 

其实不难看出上例中,call方法改变了b.setValue函数中的this指针,this从而指向了a对象的value值。
关于apply,更多的是运用在函数内部,因为在函数内部有一个arguments数组,当然也可以直接传递一个数组,这样就直接可以传入到apply方法,如下实例:

 1 var a = {
 2 
 3     value: 10
 4 
 5 };
 6 
 7 var b = {
 8 
 9     setValue: function(num){
10 
11         this.value = this.value + num;
12 
13     }
14 
15 }
16 
17 b.setValue.apply(a, [100, 20, 30]); <<执行过后,a.value等于110;