对js的闭包看了很多遍,只是每次都没有进行深入的了解,过去了就忘了。
这次也算比较深入的了解了一下吧,不过应该也有很多不足的地方,嘿嘿!
在我们开始探索闭包前,我们首先应该理解清楚一些必要的概念,这样对我们之后理解闭包有很大的帮助。
每个变量都有其作用的范围,全局变量全局有效,会一直驻扎在内存中;局部变量一般情况下会在函数执行完毕之后,就会销毁。这里要提到一个作用域链的重要概念,作用域链是当代码在一个环境中执行时创建的,作用域链的用途就是要保证执行环境中能有效有序的访问所有变量和函数。作用域链的最前端始终都是当前执行的代码所在环境的变量对象,下一个变量对象是来自其父亲环境,再下一个变量对象是其父亲的父亲环境,直到全局执行环境。其实,通俗的理解就是:在本作用域内找不到变量或者函数,则在其父亲的作用域内寻找,再找不到则到父亲的父亲作用域内寻找,直到在全局的作用域内寻找!
一般情况下呢,当函数执行完毕后,局部变量就会被销毁,内存中仅保存这全局作用域。这里也涉及到了js的垃圾回收机制,在js中有两种垃圾收集的方式:标记清除和引用计数。标记清除:垃圾收集器在运行时会给存储在内存中的所有变量都加上标记(具体的标记方式暂时就不清楚了),待变量已不被使用或者引用,去掉该标记或添加另一种标记。最后,垃圾收集器完成内存清除工作,销毁那些已无法访问到的这些变量并回收他们所占用的空间。
不过,闭包可不是说函数执行完了,变量就回收了。
这里简单解释一下闭包的概念:闭包使之有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数的内部创建另一个函数。
首先我们看一个简单的例子。
function createA(){ var c = 0; return function(){ c++; return c; } }
这就是一个简单的闭包了: var func = createA(); 从代码中可以看出,func是一个Function类型的变量,让我们执行以下:func();结果会发现输出了1。多次执行以下func()函数,发现输出的结果一直在增长。这就说明,在func()执行之后,变量c依然保存在内存中,没有被释放掉。在return的function中能够使用变量c我们能够理解,因为变量c是其父亲环境中的变量,在本环境中找不到变量c时就会去父亲环境中寻找。
再来看一个例子:
function createB(){ var result = []; for(var i=0; i<10; i++){ result[i] = function(){ return i; }; } return result; } var result = createB();
我们期望的是执行result[0]()能够返回0,执行result[1]()能够返回1,以此类推。可是实际上呢,每个函数返回的都是10.这是因为每个函数的作用域链中都保存着createB()函数的活动对象,所以它们引用的都是同一个变量i。当createB()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以每个函数内部i的值都是10.不过我们可以通过这样的设定来让闭包的行为符合我们的预期。
function createB(){ var result = []; for(var i=0; i<10; i++){ result[i] = function(num){ return function(){ return num; }; }(i); } return result; } var result = createB();
(1)在内存中维持一个变量。比如前面讲的小例子,由于闭包,函数createA中的c会一直存在于内存中,因此每次执行func(),都会给变量c加1.
(2)保护函数内的变量安全。还是以最开始的例子,函数createA()中的变量c只有内部的函数才能访问,而无法通过其他途径访问到,因此保护了变量c的安全。
(3)实现面向对象中的对象。javascript并没有提供类这样的机制,但是我们可以通过闭包来模拟出类的机制,不同的对象实例拥有独立的成员和状态。
这里我们看一个例子:
var student = function(){ var name = "bing"; var score = 80; return { getName:function(){ return name; }, setName:function(thisName){ name = thisName; } } }();
分别执行下面的语句:
student.name; student.getName(); student.setName("zhongguo"); student.getName();
可以看到,变量student是不能直接访问变量name的。只能通过getName和setName来对变量name进行读写操作。
针对第三点,我们看这样的一个例子。
function Student(){ var name = "bing"; return { getName:function(){ return name; }, setName:function(thisName){ name = thisName; } } }
分别创建两个对象stu1,stu2:
var stu1 = Student(); stu1.setName("beijing"); stu1.getName(); var stu2 = Student(); stu2.setName("shanghai"); stu2.getName(); stu1.getName();
输出的结果分别是:"beijing", "shanghai", "beijing"。可以发现stu1,stu2这两个对象之间相互独立,互不影响。
在jQuery中我们经常见这样的写法:
(function(x, y){ })(3, 5);
这是立即执行的匿名函数,匿名函数也是一种闭包。我们来看这个:
for(var i=0; i<10; i++) { (function(i){ setTimeout(function(){ console.log(i); },i*1000) })(i); }
这段代码是每隔1000ms依次输出:0, 1, 2, 3, 4, 5, 6, 7, 8, 9
如果我们不使用匿名函数,直接在for循环里写setTimeout会是什么结果呢,这就回到了第二部分讨论的问题,setTimeout也是一个函数呀,他使用了外部环境的变量i,因此每隔1000ms输出一个10,最后输出十个10。