欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JavaScript中的闭包

程序员文章站 2023-11-03 20:46:58
javascript中的闭包 闭包是指有权访问另一个函数作用域中的变量的函数。 创建闭包的常见方法,就是在一个函数内部创建另一个函数。 1.简要介绍: 一个简单的闭包例子:...

javascript中的闭包

闭包是指有权访问另一个函数作用域中的变量的函数。

创建闭包的常见方法,就是在一个函数内部创建另一个函数。

1.简要介绍:

一个简单的闭包例子:

function getname(){

var name='wenzi';

settimeout(function(){

console.log(name);

}, 500);

}

getname();

settimeout中的function是一个匿名函数,这个匿名函数里的name是getname()作用域中的变量。

还有一个很经典的例子也可以帮助我们理解什么是闭包:

function create(){

vari=0;

// 返回一个函数,暂且称之为函数a

returnfunction(){

i++;

console.log(i);

}

}

varc = create(); // c是一个函数

c(); // 函数执行

c(); // 再次执行

c(); // 第三次执行

在上面的例子中,create()返回的是一个函数,暂且称之为函数a。在函数a中,有两条语句,一条是变量i自增(i++),一条是输出语句(console.log)。第一次执行执行c()时会产生什么样的结果?输出自增后的变量i,也就是输出1;第二次执行c()会输出2;第三次执行c()时会输出3,依次累加。这个create()函数依然满足了我们在刚开始时的定义,函数a使用到了另一个函数create()中的变量i。

2.三个重要概念

2.1执行环境与变量对象

执行环境是javascript中一个重要的概念,它决定了变量或函数是否有权访问其他的数据,决定了它们各自的行为。

当某个函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[[scope]])。然后,使用this、arguments和其他命名参数的值来初始化函数的活动对象。

每个执行环境都有一个与之对应的变量对象,执行环境中定义的所有变量和函数都保存在这个对象中。虽然我们的代码无法访问这个对象,但是解析器在处理数据时会在后台使用它。

全局执行环境是最外层的一个执行环境。在web中,全局执行环境被认为是window对象,因为所有的全局变量和全局函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或者浏览器——时才会被销毁),被垃圾回收机制回收。

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入到一个环境栈中。而在函数执行后,栈将其环境弹出,把控制权返回给之前的执行环境。

2.2作用域链

作用域链是当代码在一个环境中执行时创建的,作用域链的用途就是要保证执行环境中能有效有序的访问所有变量和函数。作用域链的最前端始终都是当前执行的代码所在环境的变量对象,下一个变量对象是来自其父亲环境,再下一个变量对象是其父亲的父亲环境,直到全局执行环境。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)。其实,通俗的理解就是:在本作用域内找不到变量或者函数,则在其父亲的作用域内寻找,再找不到则到父亲的父亲作用域内寻找,直到在全局的作用域内寻找!

作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。

2.3垃圾回收机制

在js中有两种垃圾收集的方式:标记清除和引用计数。

标记清除:垃圾收集器在运行时会给存储在内存中的所有变量都加上标记(具体的标记方式暂时就不清楚了),待变量已不被使用或者引用,去掉该标记或添加另一种标记。最后,垃圾收集器完成内存清除工作,销毁那些已无法访问到的这些变量并回收他们所占用的空间。

引用计数:一般来说,引用计数的含义是跟踪记录每个值被引用的次数。当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数便是1,如果同一个值又被赋给另一个变量,则该值的引用次数加1,相反,如果包含对这个值引用的变量又取得了另一个值,则这个值的引用次数减1。当这个值的引用次数为0时,说明没有办法访问到它了,因而可以将其占用的内存空间回收。

除了一些极老版本的ie,目前市面上的js引擎基本采用标记清除来处理垃圾回收。但是需要注意的是ie中的dom由于机制问题,是采用了引用计数的方式,所以会有循环引用的问题,造成内存泄露

var ele = document.getelementbyid(“element”);

var obj = new object();

ele.obj = obj; // dom元素ele的obj引用obj变量

obj.ele = ele; // obj变量的ele引用了dom元素ele

这样就造成了循环引用的问题,导致垃圾回收机制回收不了ele和obj。不过,可以在不使用ele和obj时,对这两个变量进行 null 赋值,然后垃圾回收机制就会回收它们了。

3.理解闭包

再次拿出上面的那个例子:

function create(){

vari=0;

// 返回一个函数,暂且称之为函数a

returnfunction(){

i++;

console.log(i);

}

}

varc = create(); // c是一个函数

c(); // 函数执行

c(); // 再次执行

c(); // 第三次执行

从上面的“每个函数都有自己的执行环境”可以知道:create()函数是一个执行环境,函数a也是一个执行环境,且函数a的执行环境在create()的里面。这样就形成了一个作用域链:window->create->a。当执行c()时,函数a就会首先在当前执行环境中寻找变量i,可是没有找到,那么只能顺着作用域链向后找;在create()的执行环境中找到了,那么就可以使用了变量i了。

可是还有一个疑问,按照上面的说法,函数create()执行完毕后,这个函数与里面的变量和方法应该被销毁了,可是为什么函数c()多次执行时依然能够输出变量i呢。这就是闭包的独特之处

函数create()执行完毕后,虽然它的作用域链会被销毁,即不再存在window->create这个链式关系,但是函数a()[c()]的作用域链还依然引用着create()的变量对象,还存在着window->create->a的链式关系,导致垃圾回收机制不能回收create()的变量对象,create()的变量对象仍然停留在内存中,直到函数a()[c()]被销毁后,create()的变量对象才会被销毁。

因此,虽然create()已经执行完毕了,但是create()的变量对象并没有被回收,还停留在内存中,依然可以使用。

从上面的分析中可以看到,闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。当页面中存在过多的闭包,或者闭包的嵌套很多很深时,会导致内存占用过多。因此,在这里建议:慎用闭包

4.闭包与变量

有很多新手为dom元素绑定事件时,通常会这么写:

function bindclick(){ var li = document.getelementsbytagname('li'); // 假设一共有5个li标签 for(var i=0; i li[i].onclick = function(){ console.log('click the '+i+' li tag'); } } }

他的本意是想为每个li标签绑定一个单独的事件,点击第几个li标签,就能输出几。可是,最后的结果却是,点击哪个li标签输出的都是5,这是为什么呢?

其实这位程序员写的bindclick()已经构成了一个闭包,下面的这个函数有他的作用域,而变量i本不属于这个函数的作用域,而是属于bindclick()中的:

// 匿名函数 function(){ console.log('click the '+i+' li tag'); } 因此,这就构成了一个含有闭包的作用域链:window->bindclick->匿名函数。可是这跟输出的i有关系么?有。作用域链中的每个变量对象保存的是对变量和方法的引用,而不是保存这个变量的某一个值。当执行到匿名函数时,bindclick()其实已经执行完毕了,变量i的值就是5,此时每个匿名函数都引用着同一个变量i。 不过我们稍微修改一下,以满足我们的预期: function bindclick(){ var li = document.getelementsbytagname('li'); for(var i=0; i li[i].onclick = (function(j){ return function(){ console.log('click the '+j+' li tag'); } })(i); } }

在这里,我们使用立即执行的匿名函数来保证传入的值就是当前正在操作的变量i,而不是循环完成后的值。

这其实是因为作用域链的副作用,即闭包只能取得包含函数中任何变量的最后一个值。

下面是一个类似的例子:

function createfunctions(){

varresult=new array();

for(vari=0;i<10;i++){

result[i]=function(){

returni;

};

}

returnresult;

}

这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值,即位置0的函数返回0,位置1的函数返回1,以此类推。但实际上,每个函数都返回10。因为每个函数的作用域链中都保存着createfunctions()函数的活动对象,所以它们引用的都是同一个变量i。当createfunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10。但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期:

function createfunctions(){

varresult=new array();

for(vari=0;i<10;i++){

result[i]=function(num){

return function(){

returnnum;

};

}(i);

}

returnresult;

}

在重写了前面的createfunctions()函数后,每个函数就会返回各自不同的索引值了。在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量i。由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。

5. 闭包的应用场景

(1)在内存中维持一个变量。比如前面讲的小例子,由于闭包,函数create()中的变量i会一直存在于内存中,因此每次执行c(),都会给变量i加1.

(2)保护函数内的变量安全。还是那个小例子,函数create()中的变量c只有内部的函数才能访问,而无法通过其他途径访问到,因此保护了变量c的安全。

(3)实现面向对象中的对象。javascript并没有提供类这样的机制,但是我们可以通过闭包来模拟出类的机制,不同的对象实例拥有独立的成员和状态。

这里我们看一个例子:

function student(){

var name = 'wenzi';

return {

setname :function(na){

name = na;

},

getname :function(){

return name;

}

}

}

var stu = new student();

console.log(stu.name); // undefined

console.log(stu.getname()); // wenzi

这就是一个用闭包实现的简单的类,里面的name属性是私有的,外部无法进行访问,只能通过setname和getname进行访问。

当然,闭包还存在另外一种形式:

var a = (function(){ var num = 0; return function(){ return num++; } })() a(); // 0 a(); // 1 a(); // 2

 

 

一.什么是闭包?

闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

其实这句话通俗的来说就是:javascript中所有的function都是一个闭包。不过一般来说,嵌套的function所产生的闭包更为强大,也是大部分时候我们所谓的“闭包”。

看下面这段代码:

functiona() {

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的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scopechain)。以函数a从定义到执行的过程为例阐述这几个概念。

1.当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scopechain中只有window对象。

2.当执行函数a的时候,a会进入相应的执行环境(excutioncontext)。

3.在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。

4.然后执行环境会创建一个活动对象(callobject)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过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。

 

闭包中的this对象:

this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。

下面来看一个例子:

var name=”thewindow”;

var object={

name:”my object”,

getnamefunc:function(){

return function(){

return this.name;

};

}

};

alert(object.getnamefunc()()); //”thewindow”

以上代码先创建了一个全局变量name,又创建了一个包含name属性的对象,这个对象还包含一个方法:getnamefunc(),它返回一个匿名函数,而匿名函数又返回this.name。由于getnamefunc()返回一个函数,因此调用object.getnamefunc()()就会立即调用它返回的函数,结果就是返回一个字符串。然而,这个例子返回的字符串是”thewindow”,即全局name变量的值。为什么匿名函数没有取得其包含作用域(或内部作用域)的this对象呢?

每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了:

var name=”thewindow”;

var object={

name:”my object”,

getnamefunc:function(){

var that=this;

return function(){

returnthat.name;

};

}

};

alert(object.getnamefunc()());//”my object”

红色标注的行展示了这里与前面一个例子的不同之处。在定义匿名函数之前,我们把this对象赋值给了一个名叫that的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声明的一个变量。即使在函数返回之后,that也仍然引用着object,所以调用object.getnamefunc()()就返回了”my object”。

(arguments也存在同样的问题,如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。)

在几种特殊的情况下,this的值可能会意外的改变:

var name=”the window”;

var object={

name:”my object”,

getname:function(){

return this.name;

}

};

这里的getname()方法只简单的返回this.name的值。以下是几种调用object.getname()的方式及各自的结果:

object.getname(); //”my object”

(object.getname)(); //”my object”

(object.getname=object.getname)(); //”the window”

第一行代码跟平常一样调用了object.getname(),返回的是”my object”,因为this.name就是object.name。

第二行代码在调用这个方法前先给它加上了括号,虽然加上括号之后,就好像只是在引用一个函数,但this的值得到了维持,因为object.getname和(object.getname)的定义是相同的。

第三行代码先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以this的值不能得到维持,结果就返回了”the window”。

 

闭包中的内存泄露:

由于ie9之前的版本对javascript对象和dom对象使用不同的垃圾收集例程,因此闭包在ie的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个html元素,那么就意味着该元素将无法被销毁:

functionassignhandler(){

varelement=document.getelementbyid(“someelement”);

element.onclick=function(){

alert(element.id);

};

}

以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignhandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就永远不会被回收。这个问题可以通过稍微改写一下代码来解决:

function assignhandler(){

varelement=document.getelementbyid(“someelement”);

var id=element.id;

element.onclick=function(){

alert(id);

};

element=null;

}

在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄露的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把element变量设置为null。这样就能够解除对dom对象的引用,顺利的减少其引用数,确保正常回收其占用的内存。

;>
;>