JavaScript的闭包机制
目录
一、闭包概述
1.1、闭包的概念
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
2.1、执行环境与作用域链
当某个函数被调用时,会创建一个执行环境及相应的作用域链。
然后,使用arguments和其他命名参数的值来初始化函数的活动对象。
但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境。
function compare(value1, value2){
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
全局环境的变量对象(window)始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。
在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。
当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。
此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。
对于这个例子中compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。
一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况有所不同。
2.3、闭包示例
function createComparisonFunction(propertyName){
return function(object1, object2){ //闭包
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
}
var compare = createComparisonFunction("name");
var result = compare({
name: "Nicholas"
}, {
name: "Greg"
});
createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。
换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中。
直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁,例如:
//解除对匿名函数的引用(以便释放内存)
compare = null;
2.4、闭包的缺点
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用闭包可能会导致内存占用过多。
二、闭包的特性
2.1、闭包中的变量
闭包的一个副作用是:闭包只能取得包含函数中任何变量的最后一个值。
function createFunctions(){
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(){ //result数组保存闭包
return i;
};
}
return result;
}
var result = createFunctions();
for(var j = 0; j < 10; j++){
console.log(result[j]()); //都是打印'10'
}
因为result数组中的每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。
可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下所示:
function createFunctions(){
var result = new Array();
for(var i = 0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i); //函数对象后跟括号,函数会立即执行
}
return result;
}
var result = createFunctions();
for(var j = 0; j < 10; j++){
console.log(result[j]()); //0,1,2,3,4,5,6,7,8,9
}
在这个版本中,没有直接把闭包赋值给数组,而是定义了一个匿名函数,并立即执行该匿名函数。
由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num。
而在这个匿名函数内部,又创建并返回了一个访问num的闭包,这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。
2.2、闭包中的this对象
在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。
闭包的执行环境具有全局性,因此其this对象通常指向window。
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function(){
return function(){
return this.name;
};
}
};
console.log(object.getNameFunc()()); //"The Window"
每个函数在被调用时都会自动取得两个特殊变量:this和arguments。
内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
但是,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function(){
var that = this;
return function(){
return that.name;
};
}
};
console.log(object.getNameFunc()()); //"My Object"
arguments也存在同样的问题。如果想访问外部作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
2.3、闭包导致的内存泄露
如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){ //闭包
console.log(element.id); //循环引用了element对象
}
}
由于闭包中保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。
只要闭包存在,element的引用数至少也是1。
不过,这个问题可以通过稍微改写一下代码来解决,如下所示:
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){ //闭包
console.log(id);
};
element = null;
}
把element变量设置为null,这样就能够解除对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。
三、闭包实现的功能
3.1、使用闭包模仿块级作用域
JavaScript没有块级作用域的概念,但是可以使用闭包模仿块级作用域(通常称为私有作用域)。
(function(){
//这里是块级作用域
})();
因为JavaScript将function关键字当作函数声明的开始,而函数声明后面不能跟圆括号。
然而,函数表达式的后面可以跟圆括号,所以为函数加上圆括号,使其转换成函数表达式。
(function(){
var i = 0;
})();
console.log(i); //报错
由于变量i是在块级作用域里面声明的,所以在全局作用域中访问不到它。
3.2、私有变量
JavaScript没有私有成员的概念,所有对象属性都是公有的。
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。
私有变量包括:
- 函数的参数
- 局部变量
- 在函数内部定义的其他函数。
function add(num1, num2){
var sum = num1 + num2;
return sum;
}
在上面的函数add中,有3个私有变量:num1、num2和sum。
如果在函数内部创建一个闭包,那么闭包通过自己的作用域链可以访问私有变量。
把有权访问私有变量和私有函数的公有方法称为特权方法。
function MyObject(){
//私有变量
var privateVariable = 10;
//私有函数
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function(){
privateVariable++;
return privateFunction();
};
}
在创建MyObject的实例后,除了使用publicMethod()这一个途径外,没有任何方法而可以直接访问privateVariable和privateFunction()。
利用私有成员和特权成员,可以隐藏那些不应该被直接修改的数据,例如:
function Person(name){
this.getName = function(){
return name;
};
this.setName = function(value){
name = value;
};
}
var person = new Person("Nicholas");
console.log(person.getName()); // "Nicholas"
person.setName("Greg");
console.log(person.getName()); // "Greg"
这种方式构建私有变量有两个缺点:
- 私有变量不能由所有实例共享
- 每个实例都会创建一个特权方法,不能实现函数复用。
使用静态私有变量可以避免这些问题
3.3、静态私有变量
在私有作用域中定义私有变量或函数,同样也可以创建特权方法。
(function(){
//私有变量
var name = "";
//构造函数
Person = function(value){
name = value;
};
//特权方法
Person.prototype.getName = function(){
return name;
};
//特权方法
Person.prototype.setName = function(value){
name = value;
}
})();
var person1 = new Person("Nicholas");
console.log(person1.getName()); //"Nicholas"
person1.setName("Greg");
console.log(person1.getName); //"Greg"
var person2 = new Person("Michael");
console.log(person1.getName()); //"Michael"
console.log(person2.getName()); //"Michael"
Person在块级作用域中被声明为全局变量,能够在块级作用域之外被访问到。
特权方法作为闭包,总是保存着对包含作用域的引用。
这个模式与在构造函数中定义特权方法的主要区别,就在于
- 私有变量和私有函数是由实例共享的
- 实现了特权方法的函数复用。
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。
另外,可以将以上创建实例私有变量和创建静态私有变量的方法结合使用,使得构造函数初始化的实例可以同时拥有实例私有变量和静态私有变量。
示例:
(function(){
// name是静态私有变量
var name = "";
Person = function(nameValue, ageValue){
name = nameValue;
// age是实例私有变量
var age = ageValue;
this.getAge = function(){
return age;
};
this.setAge = function(ageValue){
age = ageValue
}
};
Person.prototype.getName = function(){
return name;
};
Person.prototype.setName = function(nameValue){
name = nameValue;
};
})();
var person1 = new Person("Nicholas", 27);
var person2 = new Person("Greg", 30);
// 因为name是静态私有变量,所以person1和person2的name属性是一样的
console.log(person1.getName()); // "Greg"
console.log(person2.getName()); // "Greg"
// 因为age是实例私有变量,所以person1和person2的age属性是不一样的
console.log(person1.getAge()); // 27
console.log(person2.getAge()); // 30
3.4、模块模式
前面的模式是用于为自定义类型创建私有变量和特权方法的,而模块模式则是为单例创建私有变量和特权方法。
单例,指的就是只有一个实例的对象。通常,JavaScript是以对象字面量的方式来创建单例对象的:
var singleTon = {
name: value,
method: function(){
//这里是方法的代码
}
};
模块模式通过为单例添加私有变量和特权方法能够使其得到增强
var application = function(){
//私有变量和函数
var components = new Array();
//初始化
components.push(new BaseComponent());
//公共
return {
getComponentCount: function(){
return components.length;
},
registerComponent: function(component){
if(typeof component == "object"){
components.push(component);
}
}
};
}(); //立即调用执行!
在Web应用程序中,经常需要使用一个单例来管理应用程序级的信息。
上面这个简单的例子创建了一个用于管理组件的application对象。
在创建这个对象的过程中,首先声明了一个私有的components数组,并向数组中添加了一个BaseComponent的新实例(在这里不需要关心BaseComponent的代码,我们只是用它来展示初始化操作)。
而返回对象的getComponentCount()和registerComponent()方法,都是有权访问数组components的特权方法。前者返回已注册的组件数目,后者用于注册新组件。
模块模式的适用条件:如果必须创建一个对象并以某些私有数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是Object的实例,因为最终要通过一个对象字面量来表示它。
3.5、增强的模块模式
对模块模式进行改进,即在返回对象之前加入对其增强的代码。
这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。
如果前面演示模块模式的例子中的application对象必须是BaseComponent的实例,那么就可以使用以下代码:
var application = function(){
//私有变量和函数
var components = new Array();
//初始化
components.push(new BaseComponent());
//创建application的一个局部副本
var app = new BaseComponent();
//公共接口
app.getComponentCount = function(){
return components.length;
};
app.registerComponent = function(component){
if(typeof component == "object"){
components.push(component);
}
};
//返回这个副本
return app;
}();