本文首发于我的博客,这是我的github,欢迎来访。
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数。
之所以一个内部的函数可以访问其外部的变量,而且在其被返回或是调用时还可以访问,是因为这个内部函数的作用域链中包含外部函数的作用域。
知识储备
在了解闭包之前,先要熟悉以下几点:
1. 首先要理解执行环境,执行环境定义了变量或函数有权访问的其他数据。
2. 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
3. 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入到一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
4. 当某个函数被调用时,会创建一个执行环境及其相应的作用域链。然后使用arguments
和其他命名参数的值来初始化函数的活动对象。在函数中,活动对象作为变量对象使用(作用域链是由每层的变量对象链起来的)。
5. 在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,直到作用域链终点即全局执行环境。
6. 作用域链的本质是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
1.一般情况下
不谈论闭包,一般的,从在全局执行环境创建一个函数开始。
在创建一个函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数内部的[[Scope]]
。
然后执行流进入这个函数,函数的执行环境被压入环境栈中,此函数执行环境的活动对象作为变量对象被创建并推入执行环境作用域链的前端。
对这个例子中的函数而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。
无论在什么时候在函数中访问变量时,会从作用域链搜索变量名。
一般情况下,函数执行完,局部活动对象就会被销毁,内存中仅有全局作用域(里边只有全局执行环境的变量对象)。
以下面这段代码为例:
function compare (value1, value2) {
//创建一个预先包含全局变量对象的作用域链,保存在[[Scope]]
if (value1 < value2) {
//访问函数变量时,即在代码最后一条语句执行过程中,会从作用域链前端开始搜索变量名
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
//执行流进入函数时,compare的执行环境压入环境栈
//compare执行环境的活动对象作为变量对象接到作用域链的前端
//函数执行完,compare执行环境弹出栈,compare活动对象销毁
如图,作用域链从0开始向后查找:
2.产生闭包的情况下
如下是一个以属性名作为参数,按其属性的值对数据进行排序的函数:
function createComparisonFunction(propertyName) {
return function(object1,object2){ //返回一个匿名函数
var value1=object1[propertyName];
var value2=object2[propertyName];
if(value1value2){
return 1;
} else {
return 0;
}
};
}
var data=[{name:"Zachary",age:28},{name:"Nicholas",age:29}];
data.sort(createComparisonFunction("name"));
console.log(data[0]); //Object {name: "Nicholas", age: 29}
data.sort(createComparisonFunction("age"));
console.log(data[0]); //Object {name: "Zachary", age: 28}
createComparisonFunction()
函数和返回的匿名函数的作用域链如下图所示:
在匿名函数从
createComparisonFunction()
中被返回后,它的作用域链被初始化为包含createComparisonFunction()
函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()
中定义的所有变量。
更为重要的是:
createComparisonFunction()
函数在执行完毕后,其他活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。
当createComparisonFunction()
函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁,createComparisonFunction()
的活动对象才会被销毁。
例如以下代码,返回的匿名函数被保存在变量compareNames
中,通过将compareNames
设置为null
来解除对匿名函数的引用,解除引用之后垃圾回收例程将会清除该匿名函数,随之该匿名函数的作用域链也会被销毁,则其作用域链上的其他作用域也会安全的销毁(全局作用域除外)。
var compareNames = createComparisonFunction("name");
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
compareNames = null;
3.经典的闭包实例
在学习闭包的时候我们很容易见到以下代码:
for(var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000);
}
如果不理解闭包,很容易认为这段代码输出的是1,2,3,4,5
,每隔一秒输出一个。但是其实是以每秒一次的频率输出5
次6
。因为延迟函数的回调会在循环执行完之后再执行(即使setTimeout
的第二个参数为0
,由于事件循环的机制,回调函数依然会在循环结束后执行)。
之所以我们错误的认为它会输出
1~5
,是因为我们自己假设每个迭代在运行时都会给自己“捕获”一个i
的副本。但是,根据作用域的工作原理,实际情况是尽管循环中五个函数是在各个迭代中分别定义的,但是它们都被封闭在同一个全局作用域中,即实际上只有一个i
。
下面我们尝试将其结果修改为输出1~5
。那么下边的代码行不行呢?
for(var i=1; i<=5; i++) {
(function(){
setTimeout( function timer() {
console.log(i);
}, i*1000);
})();
}
答案是不行。确实,上边的代码在每个迭代中都添加了一个独有的作用域,但是这个作用域是空的,没有对于i
的定义,所以每次查找i
的时候还是会向上查找,找到全局作用域中的i
并输出。在循环执行完之后的i
是6
,所以依然每次输出6
。现在我们为每个小的作用域添加一个变量,其值为循环时当前的i
的值。
for(var i=1; i<=5; i++) {
(function(i){
setTimeout( function timer() {
console.log(i);
}, i*1000);
})(i);
}
现在的结果已经和理想的情况一样了。ES6
拥有更简洁的方法创建一个封闭的作用域:let
,它会使一个块转换成一个可以被关闭的作用域(对于let
定义的变量来说块就是封闭的)。所以你可以写成下边的代码:
for(let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000);
}
4.利用闭包创建模块
我们看以下代码:
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); //cool
foo.doAnother(); //1 ! 2 ! 3
这个模式就被称为模块,最常见的实现模块模式的方法通常被称为模块暴露,这里展示其变体。
模块模式需要具备两个必要条件:
1.必须有外部的封闭函数(CoolModule()
),该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
以上就是我在学习闭包时的一些总结,欢迎讨论。
参考资料:《JavaScript高级程序设计》
《你不知道的JavaScript》