本文深入解析JavaScript闭包的核心概念,通过变量循环引用和缓存封装两个经典案例,展示闭包的实际应用。重点探讨闭包导致的内存泄漏机制及this指向陷阱,并提供使用立即执行函数和self变量引用的解决方案,帮助开发者写出更健壮、高效的代码。
JavaScript闭包基础概念
若已掌握作用域及函数作为独立对象等基础概念,理解并应用闭包便顺理成章。在DOM事件处理中,许多开发者其实已在无意识中使用闭包。尽管浏览器JavaScript引擎的Bug可能导致内存泄漏,且调试时常令人困惑,但理解其本质至关重要。简言之,因函数即对象,对象即属性集合,属性值亦可为对象,故在函数内定义函数是自然之举。若在函数func内部声明inner函数并在外部调用,即形成闭包。
代码如下:
var outter = [];
function clouseTest() {
var array = ["one", "two", "three", "four"];
for (var i = 0; i < array.length; i++) {
var x = {};
x.no = i;
x.text = array[i];
x.invoke = function () {
print(i);
}
outter.push(x);
}
}
//调用这个函数
clouseTest();
print(outter[0].invoke());
print(outter[1].invoke());
print(outter[2].invoke());
print(outter[3].invoke());运行的结果如何呢?很多初学者可能会得出这样的答案:
0
1
2
3
然而,运行这个程序,得到的结果为:
4
4
4
4
解决变量循环引用的闭包技巧
实际上,在每次迭代中,语句x.invoke = function(){print(i);}并未立即执行,仅构建了一个函数体为print(i)的函数对象。当i等于4时循环终止,外部函数返回。此时调用outter[0].invoke(),i的值仍为4,故数组中所有元素的invoke方法均返回4。解决此问题的方法是声明一个匿名函数并立即执行:
代码如下:
var outter = [];
function clouseTest2() {
var array = ["one", "two", "three", "four"];
for (var i = 0; i < array.length; i++) {
var x = {};
x.no = i;
x.text = array[i];
x.invoke = function (no) {
return function () {
print(no);
}
}(i);
outter.push(x);
}
}
clouseTest2();
</script>在此例中,为x.invoke赋值时,先运行一个返回函数的函数并立即执行。这使得x.invoke在每次迭代中相当于执行如下语句:
代码如下:
//x == 0
x.invoke = function(){print(0);}
//x == 1
x.invoke = function(){print(1);}
//x == 2
x.invoke = function(){print(2);}
//x == 3
x.invoke = function(){print(3);}如此即可得到正确结果。闭包允许引用外部函数中的变量,但它使用的是该变量在外部函数中的最终值,而非创建时的值。
闭包的用途:
闭包的实际应用场景
概念明晰后,我们探讨闭包的用途。利用闭包可实现多种功能,例如模拟面向对象代码风格,以更优雅简洁的方式表达逻辑,并在某些场景下提升执行效率。
缓存:
再看一例:假设有一个耗时较长的处理函数对象,每次调用都需长时间计算。此时可将计算结果缓存,再次调用时先查缓存,若未找到则计算并更新缓存后返回;若已存在则直接返回缓存值。
闭包能实现此功能,因其不释放外部引用,从而保留函数内部的值。
代码如下:
var CachedSearchBox = (function () {
var cache = {},
count = [];
return {
attachSearchBox: function (dsid) {
if (dsid in cache) {//如果结果在缓存中
return cache[dsid];//直接返回缓存中的对象
}
var fsb = document.getElementById(dsid);//新建
cache[dsid] = fsb;//更新缓存
if (count.length > 100) {//保正缓存的大小<=100
delete cache[count.shift()];
}
return fsb;
},
clearSearchBox: function (dsid) {
if (dsid in cache) {
cache[dsid].clearSelection();
}
}
};
})();
var obj1 = CachedSearchBox.attachSearchBox("input1");
//alert(obj1);
var obj2 = CachedSearchBox.attachSearchBox("input1");实现封装:
代码如下:
var person = function(){
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
}();
print(person.name);//直接访问,结果为undefined
print(person.getName());
person.setName("jack");
print(person.getName());得到结果如下:
undefined
default
jack
闭包的另一个重要用途是实现面向对象中的对象。传统语言提供类模板,使不同实例拥有独立成员及状态。虽JavaScript无类机制,但利用闭包可模拟此机制。以上例继续说明:
代码如下:
function Person(){
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
};
var john = Person();
print(john.getName());
john.setName("john");
print(john.getName());
var jack = Person();
print(jack.getName());
jack.setName("jack");
print(jack.getName());运行结果如下:
default
john
default
jack
闭包使用的注意事项与陷阱
使用JavaScript闭包需注意以下问题: 1.内存泄漏: 不同JavaScript解释器因自身缺陷,使用闭包可能导致内存泄漏,严重影响浏览器响应速度及用户体验,甚至导致无响应。JavaScript解释器通常采用引用计数进行垃圾回收,若对象引用计数为零则自动回收。但引入闭包后,因局部变量可能在未来被使用,垃圾回收机制不会处理这些被外部引用的局部变量。若出现循环引用(如A引用B,B引用C,C引用A),垃圾回收机制会判定引用计数不为零,从而导致内存泄漏。 2.上下文的引用:
代码如下:
$(function(){
var con = $("div#panel");
this.id = "content";
con.click(function(){
alert(this.id);//panel
});
});此处的alert(this.id)究竟引用何值?许多开发者可能误判:
content
理由是,this.id被显式赋值为content,且click回调形成的闭包会引用this.id,故返回值应为content。然而实际弹出的是“panel”。原因在于,尽管闭包可引用局部变量,但涉及this时情况微妙。因调用对象存在,当闭包被调用(即panel的click事件触发)时,此处的this引用的是con这个jQuery对象。而匿名函数中的this.id = “content”是对匿名函数本身的操作。两个this引用的并非同一对象。
如果想要在事件处理函数中访问这个值,我们必须做一些改变:
代码如下:
$(function(){
var con = $("div#panel");
this.id = "content";
var self = this;
con.click(function(){
alert(self.id);//content
});
});这样,我们在事件处理函数中保存的是外部局部变量self的引用,而非this。此技巧在实际应用中广泛使用,后续章节将详细讨论。关于闭包的更多内容,包括其他命令式语言中的“闭包”及实际项目应用等,将在第九章深入探讨。
附:由于本身水平有限,文中难免有纰漏错误等,或者语言本身有不妥当之处,欢迎及时指正,提出建议。本文只为抛砖引玉,谢谢大家!