闭包

说起闭包,它是JavaScript重要的核心技术之一,在面试以及实际应用当中,我们都离不开它们。要理解闭包,首先我们要了解一下执行环境和作用域链这两个重要概念。
一、执行环境
执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。执行环境主要包括全局环境和局部环境。

请看下面例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var color="blue";
function changecolor(){
var anothercolor="red";
function swaprcolors(){
var tempcolor=anothercolor;
anothercolor=color;
color=tempcolor;
}
swapcolors();
}
changecolor();
1、第一步,首先是全局上下文入栈。
2、全局上下文入栈之后,其中的可执行代码开始执行,直到遇到了changeColor(),changeColor会创建它自己的执行上下文,因此第二步就是changeColor的执行上下文入栈。
3、changeColor的上下文入栈之后,控制器开始执行其中的代码,遇到swapcolors()之后又创建了一个执行上下文。因此第三步是swapcolors的执行上下文入栈。
4、在swapcolors的可执行代码中,再没有遇到其他能生成执行上下文的情况,因此这段代码顺利执行完毕,swapcolors的上下文从栈中弹出。
5、swapcolors的执行上下文弹出之后,继续执行changeColor的代码,也没有再遇到其他执行上下文,顺利执行完毕之后弹出。这样,ECStack中就只剩下全局上下文了。
6、全局上下文在浏览器窗口关闭后出栈。

记住:
1、执行上下文是单线程的
2、执行上下文是同步执行的,只有栈顶的上下文处于执行中,其他上下文需要等待
3、全局上下文只有唯一的一个,它在浏览器关闭时出栈
4、每次某个函数被调用,就会有个新的执行上下文生成

二、作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的最前端,是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。作用域链中的下一个变量对象来自包含的环境,再下一个变量对象则来自下一个包含的环境,这样一直延续到全局执行环境

请看下面例子

1
2
3
4
5
6
7
8
9
function  compare(value1,value2){
if(value1<value2){
return -1
}else{
return 0;
}
}
var result = compare(5,10)
在创建compare这个函数的时候,会创建一个包含全局变量对象的作用域链,这个作用域链被保存在内部的[scope]属性中,当调用compare函数时,会为函数创建一个执行环境,然后通过复制函数的【scope】属性中的对象构建起执行环境的作用域链。然后在执行的时候会有一个compre的活动对象被创建并推入执行环境的作用域链的前端。对于这个例子的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。所以,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存仅保存全局作用域,但是闭包的情况不同

三、闭包
各种专业文献上的”闭包”定义非常抽象,其实闭包就是指有权访问另一个函数作用域中的变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。

我们来看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
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调用后被自动清除。原因在f2被返回后,它的作用域链被初始化为包含f1()函数的活动对象和全局变量对象。这样f2这个函数就可以访问f1()函数中定义的所有变量。而且在f1()函数执行完毕后,它的活动对象不会被销毁,因为f2的作用域链仍然在引用这个活动对象。直到f2函数被销毁后,f1的活动对象才会被销毁。

四、闭包的用途

1、通过返回值来获取内部函数的引用

1
2
3
4
5
6
7
8
9
10
11
function outer() {//获取函数内部的变量
var a = 999;
function inner(){
console.log(a);
}
return inner;
}
var fn = outer();
fn(); // 999
原来在函数外面是访问不到函数里面的a变量的,在这个例子中inner()通过返回值获取到outer的变量,获取完后就可以在全局中调
用,并且访问outer里面的a变量

2、实现封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(){  
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
};
var john = Person();
console.log(john.getName());
john.setName("john");
console.log(john.getName());
在Person函数之外的地方无法访问其内部的变量,而通过提供闭包的形式来访问,利用闭包可以给对象设置私有属性并利用特权
(Privileged)方法访问私有属性。闭包的另一个重要用途是实现面向对象中的对象,传统的对象语言都提供类的模板机制,这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,我们可以模拟出这样的机制。

john和jack都可以称为Person这个类的实例,因为这两个实例对name这个成员的访问是独立的,互不影响的

3、实现缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function  configCache(){
var obj={};//设置一个内部的对象 用来存储缓存数据
return {
setCache:function(k,v){//设置缓存
obj[k]=v;
},
getCache:function(k){//获取缓存
return obj[k];
}
};
}
var conf = configCache();
console.log(conf);
conf.setCache(1,'demo');
console.log(conf.getCache(1));//demo
在这里例子里,我们通过setCache这个方法来缓存数据,然后通过getCache这个方法来获取到我们的缓存数据

五、使用闭包时的注意点

1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2、闭包保存的是包含函数的整个变量对象,所取得的外部对象变量为闭包被调用时的对象变量,一般为外部函数变量的最后一个值
(完)