JS闭包及应用

定义:闭包是有权访问另一函数作用域的变量的函数。
常见创建闭包的方式:在一个函数内部创建另一个函数。

1
2
3
4
5
6
function sayHi(str){
var hello = "hello";
return function(){
alert(hello+str);
}
}

理解执行环境和作用域链

执行环境(execution context)

1.执行环境:定义了变量或函数有权访问的其他数据。
2.变量对象:每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象里。
3.某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。
4.全局执行环境:全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象。全局执行环境直到应用程序退出才会被销毁。
5.每个函数都有自己的执行环境:当执行流进入到一个函数时,函数的环境就会被推入一个环境栈中,在函数执行之后,栈将环境弹出,把控制权返回给之前的执行环境。

作用域链(scope chain)

概念

当代码在一个环境中执行时,会创建变量对象的一个作用域链。
作用域链保证对执行环境有权访问的所有变量和函数的有序访问。

内容:作用域链的前端,始终是当前执行代码所在环境的变量对象。(如果环境是函数,则其活动对象为变量对象,活动对象最开始只包含一个变量,即arguments对象)。下一个变量对象来自外部环境,下一个变量对象来自于下一个包含环境。全局执行环境的变量对象始终都是作用域链中最后一个对象。

作用域链的创建

当创建函数时: [[scope]]属性保存包含全局变量的作用域链;
当调用函数时: 创建函数执行环境(execution context);复制[[scope]]中对象构建执行环境的作用域链;活动对象(activation object)被创建并被推入执行环境作用域链前端;

1
2
3
4
5
6
function sayHi(str){
var hello = "hello";
return function(){
alert(hello+str);
}
}

当创建sayHi()函数时: [[scope]]属性保存包含全局变量对象(包含sayHi()函数)的作用域链。
当调用sayHi()函数时: 创建sayHi()函数的执行环境;复制[[scope]]中对象构建执行环境的作用域链;包含arguments,str和hello的活动对象被创建并被推入执行环境作用域链前端;

作用域链的销毁

一般情况下:当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。
有闭包的情况:在外部函数,如sayHi()函数,执行完毕后,其包含arguments,str和hello的活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。使用将sayHi设置为null的方法解除对该函数的引用,即通知垃圾回收例程将其清除,匿名函数作用域被销毁。

1
2
3
4
5
6
7
function sayHi(str){
var hello = "hello";
return function(){
alert(hello+str);
}
}
sayHi=null;

作用域链本质

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

分析调用sayHi()函数过程中的作用域链

闭包的作用域链包含它自己的作用域,包含函数的作用域和全局作用域。

关于闭包值得注意的问题

1.闭包只能取得包含函数中任何变量的最后一个值

2.当闭包为匿名函数,this对象通常指向window,除非通过apply()或call()改变函数执行环境。

原因:每个函数在被调用时都会自动取得两个特殊的变量:thisarguments,因此当内部函数在搜索这两个变量时,只会搜索到其活动对象为止。而匿名函数的this指向window。
访问外部作用域中this对象的方法:把外部作用域中的this对象保存在闭包能够访问的变量里。
修改前:

1
2
3
4
5
6
7
8
9
var name = "Window";
var object={
name : "My Object";
getNameFunc:function(){
return function(){
return this.name;//"The Window"
};
}
};

修改后:

1
2
3
4
5
6
7
8
9
10
11
var name = "Window";
var object={
name : "My Object";
getNameFunc:function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());//"My Object"

访问外部作用域中arguments对象的方法:同样需要把该对象保存在闭包能够访问的变量里。
另外,关于this对象

1
2
3
4
5
6
7
8
9
var name = "Window";
var object={
name : "My Object";
getName:function(){
return this.name;
};
}
};
(object.getName=object.getName)();//"My Object"

上例中先执行赋值语句,在调用赋值后的结果,因为此赋值表达式的值是函数本身,所以this的值不能维持。

内存

1.闭包会比其他函数占用更多内存。

2.当函数返回了一个闭包,这个函数的作用域将会在内存中保存至闭包不存在为止。

解决:将外部函数设置为null。

3.如果闭包的作用域链中保存着HTML元素,意味着该元素将无法被销毁。

解决:将HTML元素保存在外部作用域中,并在不需要时将保存HTML元素的变量设置为null。

闭包的应用

闭包模仿块级作用域

方法

创建并立即调用一个函数;
修改前:

1
2
3
4
5
6
function outputNumbers(count)
for(var i=0;i<count;i++){
alert(i);
}
alert(i);//计数
}

这里首先创建了一个outputNumbers()函数,在函数内有一个for循环,由于JavaScript没有块级作用域,所以即使在循环外也可以访问循环内的变量i。

修改后:

1
2
3
4
5
6
7
8
function outputNumbers(count)
(function(){
for(var i=0;i<count;i++){
alert(i);
}
})();
alert(i)//导致一个错误!
}

现在在for循环外创建了一个私有作用域,并产生了一个闭包,由于在匿名函数中定义的任何变量都会在执行结束后销毁,因此变量i只能在循环中使用。

应用

经常在全局作用域中被用在函数外部,限制向全局作用域中添加过多变量和函数。

作用

既可以执行其中的代码,又不会在内存中留下对该函数的引用;
函数内部的所有变量都会被立即销毁(除非将某些变量赋值给了外部作用域);
通过创建私有作用域,每个开发人员既可以使用自己的变量,又不并担心搞乱全局作用域;
减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁作用域链了;

闭包创建私有变量

任何在函数中定义的变量都可以认为是私有变量。
私有变量包括函数的参数、局部变量和在函数内部定义的其它函数。
访问私有变量和私有函数的公有方法被称为特权方法(privileged method)。

方法一:在构造函数中定义特权方法

在构造函数内部定义了所有私有变量和函数,然后创建能访问这些私有成员的特权方法。
利用私有和特权成员可以隐藏那些不应该被直接修改的数据。
例:

1
2
3
4
5
6
7
8
9
10
function MyObject(){
var privateVariable = 10;
function privarFunc(){
return false;
}
this.publicMethod = function(){
privateVariable++;
return privarFunc();
}
}

构造函数模式的缺点:每个实例都会创建同样一组新方法。静态私有变量来实现特权方法可以避免这个问题。

方法二:静态私有变量

这个模式创建一个私有作用域,并在其中封装一个构造函数及相应的方法。
例:

1
2
3
4
5
6
7
8
9
10
11
12
function(){
var privateVariable = 10;
function privarFunc(){
return false;
}
MyObject = function(){
}//全局变量
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privarFunc();
}
}();

缺点:每个实例都没有自己的私有变量。

方法三:模块模式(module pattern)

单例创建私有变量和特权方法。
模块模式使用一个返回对象的匿名函数,这个匿名函数内部首先定义了私有变量和函数,然后将一个对象字面量作为函数的值返回。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var singleton =function(){
var privateVariable = 10;
function privarFunc(){
return false;
}
return{
publicProperty : true;
publicMethod : function(){
privateVariable++;
return privarFunc();
}
}
}();

方法四:增强的模块模式

适合单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。
创建一个匿名函数,定义私有变量和方法,创建一个对象实例,为实例添加特权方法,最后返回这个实例。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var singleton = function(){
var privateVariable = 10;
function privarFunc(){
return false;
}
var object = new CustonType();
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privarFunc();
}
return object;
}();