闭包的作用域链
闭包是有权访问另一个函数作用域中的变量的函数,比如:
function createFunc(words) {
return function() {
return words;
}
}
var func = createFunc("Hello World!")
func();
// "Hello World!"
上述例子中createFunc
的返回值是一个函数(闭包),这个返回值在调用时仍然可以访问createFunc
的words
属性,这是为什么呢?还记得在之前的文章Javascript 变量、作用域和内存问题中提到的,一个函数在创建时,会生成一个内部属性[[scope]]
,这个属性包含函数被创建的作用域中对象的集合,也就包括了createFunc
的活动对象,而如果没有闭包,createFunc
的活动对象在调用结束时就可以进入GC序列,只有销毁对闭包的引用,即func = null
,才会使createFunc
的活动对象被GC。
总结闭包的作用域链如下图:
通过上述分析我们还可以看出,闭包有一个不同于普通函数的特性,就是它会携带包含它的函数的作用域,因此会占用更多内存。
闭包与变量
闭包的作用域链决定了闭包只能包含外部函数中任何变量的最终值,举个例子:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
var result = createFunctions();
result[3](); // 10
上述例子中,我们在createFunctions
函数中,通过for
循环,创建了多个函数,并期望每一个都能返回创建它时的索引值,但结果发现,每一个函数都只能返回i
的最终值(由于ECMAScript中没有块级作用域,因此i
是createFunctions
中的局部变量),之所以是这样的结果,是因为每一个闭包能够访问到的i
都是对局部变量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();
result[3](); // 3
原理就是将动态的局部变量参数化,这样每一个闭包都保存了该局部变量某个时刻的副本。
在闭包中使用this
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
,因此this
指向了window
。
上述例子中,如何使闭包可以访问object呢,可以做如下修改:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
object.getNameFunc()()
// "My Object"
内存泄漏
在之前的文章Javascript 变量、作用域和内存问题提到过IE在版本9之前,ECMAScript对象和DOM对象的GC机制不同,循环引用会导致DOM对象永远不能被回收,学习完这一章节后才发现自己经常写的一段代码就存在这样的问题!-_-,书中也提到了这个例子:
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
console.log(element.id);
};
}
上面的代码就创建了一个闭包作为element
的事件处理程序,这里的循环引用体现在element
的属性onclick
的值中存在对element
的引用,即使退出assignHandler
,element
这个DOM对象也不会被引用计数机制GC,那么不在闭包中显式地引用element
,总可以了吧,就比如:
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
console.log(id);
};
}
答案是不行的,因为闭包中无论如何都是要保存一份对 assignHandler
活动对象的引用的,自然包含element
。
之前提到过,由于闭包中保存的只是函数活动对象的引用,那么闭包中能够访问的变量就具有动态性,上个例子中,闭包由于引用了assignHandler
的活动对象,就引用了element
,而element
引用了一个DOM对象,那么,如果element
不引用DOM对象,而是其他对象,比如null
,那么element
就可以被标记清楚机制GC。
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
console.log(id);
};
element = null;
}