什么是闭包?请举例说明(⾯试题⽬)
⼀、变量的作⽤域
要理解闭包,⾸先必须理解Javascript特殊的变量作⽤域。
变量的作⽤域⽆⾮就是两种:全局变量和局部变量。
Javascript语⾔的特殊之处,就在于函数内部可以直接读取全局变量。
Js代码
  var n=999;
  function f1(){
    alert(n);
  }
  f1(); // 999
另⼀⽅⾯,在函数外部⾃然⽆法读取函数内的局部变量。
Js代码
  function f1(){
    var n=999;
  }
  alert(n); // error
这⾥有⼀个地⽅需要注意,函数内部声明变量的时候,⼀定要使⽤var命令。如果不⽤的话,你实际上声明了⼀个全局变量!
Js代码
 function f1(){
    n=999;
  }
  f1();
  alert(n); // 999
--------------------------------------------------------------------------------------------------------
⼆、如何从外部读取局部变量?
出于种种原因,我们有时候需要得到函数内的局部变量。但是,前⾯已经说过了,正常情况下,这是办不到的,只有通过变通⽅法才能实现。
那就是在函数的内部,再定义⼀个函数。
Js代码
  function f1(){
    n=999;
    function f2(){
      alert(n); // 999
    }
  }
在上⾯的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不⾏,f2内部的局部变量,对f1 就是不可见的。这就是Javascript语⾔特有的“链式作⽤域”结构(chain scope),⼦对象会⼀级⼀级地向上寻所有⽗对象的变量。所以,⽗对象的所有变量,对⼦对象都是可见的,反之则不成⽴。既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
Js代码
function f1(){
    n=999;
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
--------------------------------------------------------------------------------------------------------
三、闭包的概念
上⼀节代码中的f2函数,就是闭包。
各种专业⽂献上的“闭包”(closure)定义⾮常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。由于在Javascript语⾔中,只有函数内部的⼦函数才能读取局部变量,因此可以把闭包简单理解成“定义在⼀个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的⼀座桥梁。
--------------------------------------------------------------------------------------------------------
四、闭包的⽤途
闭包可以⽤在许多地⽅。它的最⼤⽤处有两个,⼀个是前⾯提到的可以读取函数内部的变量,另⼀个就是让这些变量的值始终保持在内存中。
怎么来理解这句话呢?请看下⾯的代码。
Js代码
 function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它⼀共运⾏了两次,第⼀次的值是999,第⼆次的值是1000。这证明了,函数f1中的局部变量n⼀直保存在内存中,并没有在f1调⽤后被⾃动清除。
为什么会这样呢?原因就在于f1是f2的⽗函数,⽽f2被赋给了⼀个全局变量,这导致f2始终在内存中,⽽f2的存在依赖于f1,因此f1也始终在内存中,不会在调⽤结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另⼀个值得注意的地⽅,就是“nAdd=function(){n+=1}”这⼀⾏,⾸先在nAdd前⾯没有使⽤var关键字,因此 nAdd是⼀个全局变量,⽽不是局部变量。其次,nAdd的值是⼀个匿名函数(anonymous function),⽽这个匿名函数本⾝也是⼀个闭包,所以nAdd相当于是⼀个setter,可以在函数外部对函数内部的局部变量进⾏操作。
--------------------------------------------------------------------------------------------------------
五、使⽤闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很⼤,所以不能滥⽤闭包,否则会造成⽹页的性能问题,在IE中可能导致内存泄露。解决⽅法是,在退出函数之前,将不使⽤的局部变量全部删除。
2)闭包会在⽗函数外部,改变⽗函数内部变量的值。所以,如果你把⽗函数当作对象(object)使⽤,把闭包当作它的公⽤⽅法(Public Method),把内部变量当作它的私有属性(private value),这时⼀定要⼩⼼,不要随便改变⽗函数内部变量的值。
--------------------------------------------------------------------------------------------------------
六、思考题
如果你能理解下⾯代码的运⾏结果,应该就算理解闭包的运⾏机制了。
Js代码
  var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
     };
    }
};
NameFunc()());  //"The Window"
我们知道,this对象是在运⾏时基于函数的执⾏环境绑定的,在全局函数中,this等于window,⽽当函数被作为某个对象的⽅法调⽤时,this 等于那个对象。不过,匿名函数的执⾏环境具有全局性。每个函数在被调⽤时,其活动对象都会⾃动取得两个特殊变量:this和arguments.内部函数在搜索这两个变量时,
只会搜索到其活动对象为⽌,因此永远不可能直接访问外部函数中的这两个变量。不过可以通过把外部作⽤域中的this对象保存在⼀个闭包能够访问到的变量⾥,就可以让闭包访问到该对象了。如下所⽰:
var name = "The Window"; 
var object = {   
name: "My Object",
getNameFunc: function() {
var that = this;     
return function() {       
return that.name;     
};   
}
};
NameFunc()()); //"My Object"
这⾥在定义匿名函数之前,我们把this对象赋值给了⼀个名叫that的变量,⽽在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声明的⼀个变量。即使在函数返回之后,that也仍然引⽤着object,所以调⽤NameFunc()()就返回了"My Object"。(this和arguments也存在同样的问题,如果想访问作⽤域中的arguments对象,必须将该对象的引⽤保存到另⼀个闭包能够访问的变量中。)
--------------------------------------------------------------------------------------------------------
Javascript闭包例⼦
function outerFun()
{
var a=0;
function innerFun()
{
a++;
alert(a);
}
}
innerFun()
上⾯的代码是错误的.innerFun()的作⽤域在outerFun()内部,所在outerFun()外部调⽤它是错误的.
改成如下,也就是闭包:
Js代码
function outerFun()
{
var a=0;
function innerFun()
{
a++;
alert(a);
}
return innerFun;  //注意这⾥
}
var obj=outerFun();
obj();  //结果为1
obj();  //结果为2
var obj2=outerFun();
obj2();  //结果为1
obj2();  //结果为2
什么是闭包:
当内部函数在定义它的作⽤域的外部被引⽤时,就创建了该内部函数的闭包 ,如果内部函数引⽤了位于外部函数的变量,当外部函数调⽤完毕后,这些变量在内存不会被释放,因为闭包需要它们.
--------------------------------------------------------------------------------------------------------
再来看⼀个例⼦
Js代码
function outerFun()
{
var a =0;
alert(a);
}
var a=4;
outerFun();
alert(a);
结果是 0,4 .  因为在函数内部使⽤了var关键字维护a的作⽤域在outFun()内部.
再看下⾯的代码:
Js代码
function outerFun()
{
//没有var
函数prototype
a =0;
alert(a);
}
var a=4;
outerFun();
alert(a);
结果为 0,0 真是奇怪,为什么呢?
作⽤域链是描述⼀种路径的术语,沿着该路径可以确定变量的值 .当执⾏a=0时,因为没有使⽤var关键字,因此赋值操作会沿着作⽤域链到var
a=4;  并改变其值.
--------------------------------------------------------------------------------------------------------------------------------------------------
⼀、什么是闭包?
官⽅”的解释是:闭包是⼀个拥有许多变量和绑定了这些变量的环境的表达式(通常是⼀个函数),因⽽这些变量也是该表达式的⼀部分。相信很少有⼈能直接看懂这句话,因为他描述的太学术。其实这句话通俗的来说就是:Javascript中所有的function都是⼀个闭包。不过⼀般来说,嵌套的function所产⽣的闭包更为强⼤,也是⼤部分时候我们所谓的“闭包”。看下⾯这段代码:
function a() {
var i = 0;
function b() { alert(++i); }
return b;
}
var c = a();
c();
这段代码有两个特点:
1、函数b嵌套在函数a内部;
2、函数a返回函数b。
引⽤关系如图:
  这样在执⾏完var c=a()后,变量c实际上是指向了函数b,再执⾏c()后就会弹出⼀个窗⼝显⽰i的值(第⼀次为1)。这段代码其实就创建了⼀个闭包,为什么?因为函数a外的变量c引⽤了函数a内的函数b,就是说:
  当函数a的内部函数b被函数a外的⼀个变量引⽤的时候,就创建了⼀个闭包。
  让我们说的更透彻⼀些。所谓“闭包”,就是在构造函数体内定义另外的函数作为⽬标对象的⽅法函数,⽽这个对象的⽅法函数反过来引⽤外层函数体中的临时变量。这使得只要⽬标对象在⽣存期内始终能保持其⽅法,就能间接保持原构造函数体当时⽤到的临时变量值。尽管最开始的构造函数调⽤已经结束,临时变量的名称也都消失了,但在⽬标对象的⽅法内却始终能引⽤到该变量的值,⽽且该值只能通这种⽅法来访问。即使再次调⽤相同的构造函数,但只会⽣成新对象和⽅法,新的临时变量只是对应新的值,和上次那次调⽤的是各⾃独⽴的。
⼆、闭包有什么作⽤?
  简⽽⾔之,闭包的作⽤就是在a执⾏完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a所占⽤的资源,因为a的内部函数b的执⾏需要依赖a中的变量。这是对闭包作⽤的⾮常直⽩的描述,不专业也不严谨,但⼤概意思就是这样,理解闭包需要循序渐进的过程。
在上⾯的例⼦中,由于闭包的存在使得函数a返回后,a中的i始终存在,这样每次执⾏c(),i都是⾃加1后alert出i的值。
  那么我们来想象另⼀种情况,如果a返回的不是函数b,情况就完全不同了。因为a执⾏完后,b没有被返回给a的外界,只是被a所引⽤,⽽此时a也只会被b引⽤,因此函数a和b互相引⽤但⼜不被外界打扰(被外界引⽤),函数a和b就会被GC回收。(关于Javascript的垃圾回收机制将在后⾯详细介绍)
三、闭包内的微观世界
  如果要更加深⼊的了解闭包以及函数a和嵌套函数b的关系,我们需要引⼊另外⼏个概念:函数的执⾏环境(excution context)、活动对象(call object)、作⽤域(scope)、作⽤域链(scope chain)。以函数a从定义到执⾏的过程为例阐述这⼏个概念。
1. 当定义函数a的时候,js解释器会将函数a的作⽤域链(scope chain)设置为定义a时a所在的“环境”,如果a是⼀个全局函数,则scope
chain中只有window对象。
2. 当执⾏函数a的时候,a会进⼊相应的执⾏环境(excution context)。
3. 在创建执⾏环境的过程中,⾸先会为a添加⼀个scope属性,即a的作⽤域,其值就为第1步中的scope chain。即a.scope=a的作⽤域
链。
4. 然后执⾏环境会创建⼀个活动对象(call object)。活动对象也是⼀个拥有属性的对象,但它不具有原型⽽且不能通过Javascript代码直接
访问。创建完活动对象后,把活动对象添加到a的作⽤域链的最顶端。此时a的作⽤域链包含了两个对象:a的活动对象和window对象。
5. 下⼀步是在活动对象上添加⼀个arguments属性,它保存着调⽤函数a时所传递的参数。
6. 最后把所有函数a的形参和内部的函数b的引⽤也添加到a的活动对象上。在这⼀步中,完成了函数b的的定义,因此如同第3步,函数b
的作⽤域链被设置为b所被定义的环境,即a的作⽤域。
到此,整个函数a从定义到执⾏的步骤就完成了。此时a返回函数b的引⽤给c,⼜函数b的作⽤域链包含了对函数a的活动对象的引⽤,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引⽤,函数b⼜依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执⾏的时候亦会像以上步骤⼀样。因此,执⾏时b的作⽤域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所⽰:
如图所⽰,当在函数b中访问⼀个变量的时候,搜索顺序是:
1. 先搜索⾃⾝的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查,直到到为⽌。
2. 如果函数b存在prototype原型对象,则在查完⾃⾝的活动对象后先查⾃⾝的原型对象,再继续查。这就是Javascript中的变量查
机制。
3. 如果整个作⽤域链上都⽆法到,则返回undefined。
⼩结,本段中提到了两个重要的词语:函数的定义与执⾏。⽂中提到函数的作⽤域是在定义函数时候就已经确定,⽽不是在执⾏的时候确定(参看步骤1和3)。⽤⼀段代码来说明这个问题:
function f(x) {
var g = function () { return x; }
return g;
}
var h = f(1);
alert(h());
这段代码中变量h指向了f中的那个匿名函数(由g返回)。
假设函数h的作⽤域是在执⾏alert(h())确定的,那么此时h的作⽤域链是:h的活动对象->alert的活动对象->window对象。
假设函数h的作⽤域是在定义时确定的,就是说h指向的那个匿名函数在定义的时候就已经确定了作⽤域。那么在执⾏的时候,h的作⽤域链为:h的活动对象->f的活动对象->window对象。
如果第⼀种假设成⽴,那输出值就是undefined;如果第⼆种假设成⽴,输出值则为1。
运⾏结果证明了第2个假设是正确的,说明函数的作⽤域确实是在定义这个函数的时候就已经确定了。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。