JavaScript 闭包机制详解:从原理到实战应用
本文深入解析JavaScript闭包的核心原理,通过代码示例阐明内部函数如何访问外部变量及内存锁定机制。文章进一步探讨闭包在数组排序、映射、过滤及延迟调用等场景中的实际应用,并指出对象引用与数值传递的区别,帮助开发者掌握闭包的优雅设计与潜在陷阱。
闭包的基本概念与常见误区
伟大的爱因斯坦同志说过:“如果你无法向一个 6 岁小孩解释清楚某问题,那说明你自己都没整明白”。然而,当我向一个 27 岁的朋友解释什么是闭包时,却彻底失败了。
这源自国外某用户在Stack Overflow上关于JavaScript闭包的提问。鉴于该平台的性质,众多高手提供了精彩解答,其中部分回答堪称经典,例如以下示例。
当在一个外部函数内部定义一个内部函数(即函数嵌套)时,内部函数有权访问外部函数的变量。
function foo(x) {
var tmp = 3;
function bar(y) {
alert(x + y + (++tmp));
}
bar(10);
}
foo(2); // alert 16
foo(2); // alert 16
foo(2); // alert 16
上述代码可正确执行并返回结果16,因为bar函数能够访问外部函数的变量tmp,同时也能访问外部函数foo的参数x。但需注意,上述示例并不构成闭包。
闭包的实现机制与内存管理
若要实现闭包,需将内部函数作为外部函数的返回值返回。在返回前,内部函数会将所有已访问过的外部函数变量在内存中锁定,即这些变量常驻bar的内存空间,不会被垃圾回收器回收,示例如下。
function foo(x) {
var tmp = 3;
return function (y) {
alert(x + y + (++tmp));
}
}
var bar = foo(2); // bar 现在是个闭包了
bar(10); // alert 16
bar(10); // alert 17
bar(10); // alert 18
在上述代码中,第一次执行bar时仍返回结果16,因为bar依然可以访问x及tmp,尽管它们已不直接存在于foo的作用域内。由于tmp被锁定在bar的闭包中,每次执行bar时tmp都会自增,因此第二次和第三次执行bar时,分别返回17和18。
在此示例中,x仅为一个纯数值。当foo被调用时,数值x会作为参数被拷贝至foo内部。
对象引用与数值传递的区别
然而,JavaScript处理对象时始终使用引用。若以对象作为参数调用foo,foo中传入的实际上是原始对象的引用,因此该原始对象也相当于被闭包,示例如下。
function foo(x) {
var tmp = 3;
return function (y) {
alert(x + y + tmp++);
x.memb = x.memb ? x.memb + 1 : 1;
alert(x.memb);
}
}
var age = new Number(2);
var bar = foo(age); // bar 现在是个闭包了
bar(10); // alert 15 1
bar(10); // alert 16 2
bar(10); // alert 17 3
如预期所示,每次执行bar(10)时,不仅tmp自增,x.memb也会自增,因为函数体内的x与函数体外的age引用的是同一对象。
来源:http://stackoverflow.com/questions/111102/how-do-javascript-closures-work
补充说明:通过上述示例,应能较为清晰地理解闭包。若自认已理解,可尝试推测以下代码的执行结果。
function foo(x) {
var tmp = 3;
return function (y) {
alert(x + y + tmp++);
x.memb = x.memb ? x.memb + 1 : 1;
alert(x.memb);
}
}
var age = new Number(2);
var bar1 = foo(age); // bar1 现在是个闭包了
bar1(10); // alert 15 1
bar1(10); // alert 16 2
bar1(10); // alert 17 3
var bar2 = foo(age); // bar2 现在也是个闭包了
bar2(10); // alert ? ?
bar2(10); // alert ? ?
bar2(10); // alert ? ?
bar1(10); // alert ? ?
bar1(10); // alert ? ?
bar1(10); // alert ? ?
闭包在高阶函数中的实际应用
在实际应用中,闭包可构建出极为优雅的设计,允许对funarg上定义的多种计算方式进行定制。以下是数组排序的示例,它接受一个排序条件函数作为参数。
[1, 2, 3].sort(function (a, b) {
... // 排序条件
});
类似的例子还包括数组的map方法,该方法根据函数中定义的条件将原数组映射到一个新数组中。
[1, 2, 3].map(function (element) {
return element * 2;
}); // [2, 4, 6]
使用函数式参数,可便捷地实现一个搜索方法,并支持无限制的搜索条件。
someCollection.find(function (element) {
return element.someProperty == 'searchCondition';
});
此外还有应用函数,例如常见的forEach方法,将函数应用于每个数组元素。
[1, 2, 3].forEach(function (element) {
if (element % 2 != 0) {
alert(element);
}
}); // 1, 3
顺便提及,函数对象的apply和call方法在函数式编程中也可用作应用函数。此处,我们将它们视为应用到参数中的函数(在apply中为参数列表,在call中为独立参数)。
(function () {
alert([].join.call(arguments, ';')); // 1;2;3
}).apply(this, [1, 2, 3]);
闭包的其他重要应用场景
闭包的另一个重要应用在于延迟调用。
var a = 10;
setTimeout(function () {
alert(a); // 10, after one second
}, 1000);
还有回调函数:
//...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
// 当数据就绪的时候,才会调用;
// 这里,不论是在哪个上下文中创建
// 此时变量“x”的值已经存在了
alert(x); // 10
};
//...
还可以创建封装的作用域来隐藏辅助对象。
var foo = {};
// 初始化
(function (object) {
var x = 10;
object.getX = function _getX() {
return x;
};
})(foo);
alert(foo.getX()); // 获得闭包 "x" – 10
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
