前端开发必须知道的JS之闭包及应用
本文深入解析JavaScript中函数闭包的原理,详细阐述执行环境、词法与动态作用域及调用对象的关系。通过具体代码示例,展示如何利用闭包解决排序状态保存、事件循环变量捕获、缓存管理及程序暂停等实际问题,并提醒滥用闭包可能导致内存泄露的风险。
引言
在前一篇关于前端开发中JS原型与继承的文章里,我曾提及后续将探讨闭包。鉴于近期愈发意识到强化闭包应用能力的必要性,这篇文章的撰写已刻不容缓。本文聚焦于函数层面的闭包,不涉及对象层面的闭包(例如通过with语句实现的闭包)。若文中观点存在偏差,欢迎指正与讨论。
一. 闭包的理论
闭包理论基础
首先,必须掌握以下几个核心概念:
执行环境
每当调用一个函数(即执行函数)时,系统会为该函数创建一个封闭的局部运行环境,称为该函数的执行环境。函数始终在其自身的执行环境中运行,包括读写局部变量、函数参数以及执行内部逻辑。创建执行环境的过程同时也创建了函数的作用域,函数正是在其自身的作用域下执行。换个角度理解,每个函数的执行环境都关联着一个作用域链,子函数的作用域链包含其父函数的作用域链。关于作用域与作用域链的更多细节,请参阅下文。
作用域、作用域链、调用对象
函数作用域主要分为词法作用域和动态作用域两种类型。
词法作用域指的是函数定义时的作用域,也称为静态作用域。当函数被定义时,其词法作用域即已确定,它描述了在函数结构的嵌套关系下,函数作用的范围。此时,该函数的作用域链便已形成。作用域链是将这些具有嵌套层级关系的作用域串联起来的链条。函数的内部[[scope]]属性指向了这个作用域链。
动态作用域指的是函数调用执行时的作用域。当函数被调用时,系统首先将函数内部的[[scope]]属性指向函数的作用域链,随后创建一个调用对象,利用该调用对象记录函数参数和局部变量,并将其置于作用域链的顶部。动态作用域正是通过将调用对象添加到作用域链顶部来创建的。此时,[[scope]]不仅包含定义时的作用域链,还包含了调用时创建的调用对象。换言之,执行环境下的作用域等于函数定义时确定的作用域链加上刚刚创建的调用对象,从而形成了新的作用域链。因此,这被称为动态作用域,且作用域链随之发生变化。回顾这里的作用域,它实际上是一个对象链,这些对象是函数调用时创建的调用对象,以及其上层层层调用对象,直到最顶层的全局对象。
例如,在全局环境下的函数A内部嵌套了函数B,那么函数B的作用域链为:函数B的作用域 -> 函数A的作用域 -> 全局window的作用域。当调用函数B并查找某标识符时,会按照函数B的作用域 -> 函数A的作用域 -> 全局window的作用域进行查找,实际上则是按照函数B的调用对象 -> 函数A的调用对象 -> 全局对象的顺序进行查找。也就是说,当函数调用时,函数的作用域链实际上是调用对象链。
闭包
在动态执行环境中,数据实时发生变化。为了保持这些非持久型变量的值,我们使用闭包这种载体来存储这些动态数据(看完后续的应用示例后,您将很好地体会这句话)。闭包的定义是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
闭包是指嵌套在函数内部的内部函数,并且该内部函数可以访问外部函数中声明的所有局部变量、参数和其他内部函数。当该内部函数在外部函数之外被调用时,就生成了闭包。(实际上,任何函数都是全局作用域的内部函数,都能访问全局变量,因此都是window的闭包)。
以下是一个示例:
代码如下:
<script type="text/javascript">
function f(x) {
var a = 0;
a++;
x++;
var inner = function() {
return a + x;
}
return inner;
}
var test = f(1);
alert(test());
</script> 垃圾回收机制:如果某个对象不再被引用,该对象将被回收。
结合前面所讲的概念,在执行var test=f(1)时创建了f的调用对象,这里暂且记作obj。执行完后虽然退出了外部执行环境,但内部函数inner被外部函数f外面的一个变量test引用。由于外部函数创建的调用对象obj有一个属性指向此内部函数,而现在这个内部函数又被引用,所以调用对象obj会继续存在,不会被垃圾回收器回收,其函数参数x和局部变量a都会在这个调用对象中得以维持。虽然调用对象不能被直接访问,但是该调用对象已成为内部函数作用域链中的一部分,可以被内部函数访问并修改,所以执行test()时,可以正确访问x和a。因此,当执行了外部函数时,生成了闭包,被引用的外部函数的变量将继续存在。
二. 闭包的应用
闭包的实际应用
应用1:
这是我在使用JS模拟排序算法过程中遇到的问题。我需要输出每一次插入排序后的数组状态。如果在循环中写成
setTimeout(function() { $("proc").innerHTML += arr + "<br/>"; }, i * 500);
会发现每次输出的都是最终排好序的数组,因为arr数组不会为你保留每次排序的状态值。为了保存会不断发生变化的数组值,我们用外面包裹一层函数来实现闭包,用闭包存储这个动态数据。下面用了2种方式实现闭包,一种是用参数存储数组的值,一种是用临时变量存储,后者必须要深拷贝。所有要通过闭包存储非持久型变量,均可以用临时变量或参数两种方式实现。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <body> <div> var a = [4, 5, 6, 8, 7, 9, 3, 2, 1, 0];</div> <div> <input type="button" value="插入排序" onclick="Sort.Insert();" /> </div> Proc: <div id="proc"> </div> </body> </html> [Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]
应用2:
这个是无忧上的例子(点击这里查看原帖),为每个<li>结点绑定click事件弹出循环的索引值。起初写成
id.onclick = function(){ alert(i); } id.onclick = function(){alert(i);}
发现最终弹出的都是4,而不是想要的 1、2、3,因为循环完毕后i值变成了4。为了保存i的值,同样我们用闭包实现:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <body> <ul> <li id="a1">aa</li> <li id="a2">aa</li> <li id="a3">aa</li> </ul> </body> </html> [Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]
(ps:var a = (function(){})(); 与 var a =new function(){}效果是一样的,均表示自执行函数。)
应用3:
下面的code是缓存的应用,catchNameArr。在匿名函数的调用对象中保存catch的值,返回的对象由于被CachedBox变量引用导致匿名函数的调用对象不会被回收,从而保持了catch的值。可以通过CachedBox.getCatch("regionId");来操作,若找不到regionId则从后台取,catchNameArr 主要是为了防止缓存过大。
代码如下:
<script type="text/javascript">
var CachedBox = (function() {
var cache = {}, catchNameArr = [], catchMax = 10000;
return {
getCatch: function(name) {
if (name in cache) {
return cache[name];
}
var value = GetDataFromBackend();
cache[name] = value;
catchNameArr.push(name);
this.clearOldCatch();
return value;
},
clearOldCatch: function() {
if (catchNameArr.length > catchMax) {
delete cache[catchNameArr.shift()];
}
}
};
})();
</script> 同理,也可以用这种思想实现自增长的ID。
代码如下:
<script type="text/javascript">
var GetId = (function() {
var id = 0;
return function() {
return id++;
}
})();
var newId1 = GetId();
var newId2 = GetId();
</script> 应用4:
这个是无忧上月MM的例子(点击这里查看原帖),用闭包实现程序的暂停执行功能,还蛮创意的。
<input type="button" value="继续" onclick='st();'/> [Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]
把这个作用延伸下,我想到了用他来实现window.confirm。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <style type="text/css"> body { font-family: Arial; font-size: 13px; background-color: #FFFFFF; } #confirmDiv { width: 200px; height: 100px; border: dashed 1px black; position: absolute; left: 200px; top: 150px; } </style> </head> <body> <div> <input name="btn2" type="button" value="删除" onclick="doConfirm('confirmDiv');" /> <div id="confirmDiv" style="display: none;"> <div style='position: absolute; left: 50px; top: 15px;'> <p> 你确定要删除吗?</p> <input type="button" value="确定" onclick="doConfirm('confirmDiv')(true);" /> <input type="button" value="取消" onclick="doConfirm('confirmDiv')(false);" /> </div> </div> </div> </body> </html> [Ctrl+A 全选 注:引入外部Js需再刷新一下页面才能执行]
总结与注意事项
看了上面的这些应用,再回到前面的一句话:在动态执行环境中,数据实时地发生变化,为了保持这些非持久型变量的值,我们用闭包这种载体来存储这些动态数据。这就是闭包的作用。也就说遇到需要存储动态变化的数据或将被回收的数据时,我们可以通过外面再包裹一层函数形成闭包来解决。
当然,闭包会导致很多外部函数的调用对象不能释放,滥用闭包会使得内存泄露,所以在频繁生成闭包的情景下我们要估计下他带来的副作用。
毕了。希望能对大家有所帮助。
者:JayChow
出处:http://ljchow.cnblogs.com
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
