javascript闭包


JavaScript闭包


闭包

变量的作用域

(1)要理解闭包,首先必须理解javascript特殊的变量作用域,变量的作用无非是两种:全局变量和局部变量,js语言的特殊之处,就在于函数内部可以直接读取全局变量。

1
2
3
4
5
var n = 999;
function f1(){
alert(n);
}
f1(); //999

(2)另一方面,在函数的外部无法读取函数内部的局部变量

1
2
3
4
function f1(){
var n = 999;
}
alert(n); //error

注意:函数内部声明变量时要用 var,否则就是全局变量

1
2
3
4
5
function f1(){
n = 999;
}
f1();
alert(n); //999

如何从外部读取局部变量

在函数的内部再定义一个函数

1
2
3
4
5
6
function f1(){
n = 999;
function f2(){
alert(n); //999
}
}

js语言“链式作用域”结构(chain scope):函数f2在函数f1内部,这时f1内部所有局部变量,对于f2都是可见,反过来则不行。子对象会一级一级地向上寻找所有父对象的变量,所有对于父对象的所有变量,对子对象都是可见的,反之则不行。既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,就可以在f1外部读取它的内部变量

1
2
3
4
5
6
7
8
9
10
function f1(){
n = 999;
function f2(){
alert(n);
}
return f2;
}

var result = f1();
result(); //999

闭包的概念

简而言之,闭包就是可以读取其他函数内部变量的函数,本质上,闭包就是将函数内部与函数外部连接起来的一座桥梁

闭包的用途

(1)读取函数内部的变量
(2)让函数内部的变量的值始终保持在内存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function f1(){
var n = 999;
nAdd = function(){
n+=1
}
function f2(){
alert(n);
}
return f2;
}
var result = f1();
result(); //999
nAdd();
result(); //1000

函数f1是f2的父函数,而f2被赋予了一个全局变量,这导致了f2始终在内存中,而f2的存在依赖于f1,因而f1也始终存在于内存中,不会在调用结束后被回收。
另外 nAdd=function(){n+=1} nAdd是一个全局变量,它的值是一个匿名函数,这个匿名函数本身也是一个全局变量。

使用闭包的注意点

(1)内存泄露,造成网页性能下降,退出函数之前,将不使用的局部变量都删除
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

具体案例分析

1
2
3
4
5
6
7
8
9
10
function a(){
var i = 0;
function b(){
alert(++i);
}
return b;
}

var c = a();
c();

代码特点:
1.函数b嵌套在函数a内部
2.函数a返回函数b
引用关系如下图

alt text
var c = a() 实际上,变量c指向了函数b
当函数a的内部函数被函数a外的一个变量引用时候,就创建了一个闭包
透彻点说,所谓的“闭包”,就是在构造体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量,这使得只要目标对象在生存期内始终能保存其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用以及结束,临时变量的名称也都消失了,但在目标对象的方法内却始终能引用到该变量的值而且该值只能通过这种方法来访问,即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新的值,和上次的调用都是各自独立的。

闭包内的微观世界

函数的执行环境(excution context)\活动对象(call object)\作用域(scope)\作用域链(scope chain)
· 当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的环境,如果a是一个全局函数,则scope chain 中只有window对象
· 当执行函数a的时候,a会进入相应的执行环境(excution context)
· 在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就是为第一步中的scope chain,即a.scope = a的作用域链
· 然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过javaScript代码直接访问,创建完活动对象,把活动对象添加到a的作用域链的最顶端,此时a的作用域脸包含了两个对象:a的活动对象和window对象
· 下一步就是在活动对象上添加一个arguments属性,它保存着调用函数时a所传递的函数
· 最后就把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上,在这一步中,完成了函数b的定义,函数b的作用域链被设置为b所定义的环境,就是a的作用域

到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:
alt text
如图所示,当在函数b中访问一个变量的时候,搜索顺序是:
· 先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。
· 如果函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
· 如果整个作用域链上都无法找到,则返回undefined。

0%