JavaScript 闭包深度解析:原理、应用与常见陷阱
本文深入解析 JavaScript 闭包的核心机制,涵盖函数中的函数、作用域链及闭包在生成函数工厂和实现私有变量中的应用。重点分析循环中闭包的常见陷阱及 ES6 let 的解决方案,并探讨闭包对内存和性能的影响,提供实用的优化建议。
简介
闭包是 JavaScript 中一项极具威力的特性。其核心定义在于“函数中的函数”,即内部函数能够访问外部函数的作用域,借此可以实现许多复杂且强大的功能。
本文将详细阐述闭包的工作原理与用法。
函数中的函数
前文提到,内部函数可以访问父函数的变量。以下是一个基础示例:
function parentFunction() {
var address = 'flydean.com';
function alertAddress() {
alert(address);
}
alertAddress();
}
parentFunction();
在示例中,我们在 parentFunction 内定义了变量 address,并在其内部定义了 alertAddress 方法。该方法内部直接访问了外部函数定义的 address 变量。
上述代码运行正常,能够正确访问并获取数据。
Closure 闭包
既然存在函数嵌套,那么什么是真正的闭包呢?
请看下面的示例:
function parentFunction() {
var address = 'flydean.com';
function alertAddress() {
alert(address);
}
return alertAddress;
}
var myFunc = parentFunction();
myFunc();
此例与前例相似,区别在于我们将内部函数作为返回值返回,并赋值给变量 myFunc。
随后,我们直接调用了 myFunc。
在 myFunc 执行时,它访问了 parentFunction 中的 address 变量,尽管此时 parentFunction 已经执行完毕并返回。
即便 parentFunction 已结束,调用 myFunc 时仍能访问到 address 变量,这就是闭包的体现。
利用这一特性,我们可以使用闭包来构建 function factory(函数工厂),示例如下:
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
其中 add5 和 add10 均为闭包,由 makeAdder 这个函数工厂创建。通过传入不同的 x 参数,我们得到了基于不同基数的加法方法。
最终生成了两个独立的加法方法。
借助函数工厂的概念,我们可以考虑闭包的实际应用场景。例如,页面上有三个按钮,点击它们可修改字体大小。
我们可以先通过函数工厂生成三个方法:
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
得到这三个方法后,将 DOM 元素与回调方法绑定:
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
使用闭包实现私有方法
与 Java 中的 private 访问修饰符不同,Java 可通过 private 指定方法仅在类内部访问。
虽然 JavaScript 没有此关键字,但我们可以利用闭包达到相同效果。
var counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
};
})();
console.log(counter.value()); // 0.
counter.increment();
counter.increment();
console.log(counter.value()); // 2.
counter.decrement();
console.log(counter.value()); // 1.
我们在父函数中定义了 privateCounter 属性和 changeBy 方法,但这些仅能在内部函数中访问。
通过闭包概念,我们将这些属性和方法封装起来,仅暴露必要接口给外部使用,从而实现私有变量和方法的封装效果。
闭包的作用域链
每个闭包都拥有作用域范围,包括函数自身的作用域、父函数的作用域以及全局作用域。
若在函数内部嵌入新函数,便会形成作用域链,称为 scope chain。
看下面的一个示例:
// global scope
var e = 10;
function sum(a){
return function(b){
return function(c){
// outer functions scope
return function(d){
// local scope
return a + b + c + d + e;
}
}
}
}
console.log(sum(1)(2)(3)(4)); // log 20
闭包的常见问题
第一个常见问题是在循环遍历中使用闭包,请看示例:
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
示例中创建了 setupHelp 函数,onfocus 方法被赋予闭包,因此闭包中的 item 可以访问外部函数定义的 item 变量。
由于在循环内赋值,我们实际上创建了三个闭包,但这三个闭包共享同一个外部函数的作用域范围。
我们的初衷是让不同 id 触发不同的帮助消息,但实际执行发现,无论哪个 id,最终显示的消息都是最后一条。
因为 onfocus 在闭包创建完成后才触发,此时 item 的值已发生变化。循环结束后,item 指向最后一个元素,因此全部显示最后一条数据的帮助消息。
如何解决此问题?
最简单的方法是使用 ES6 引入的 let 关键字,将 item 定义为块级作用域。每次循环都会创建新的 item,从而保持闭包中 item 的值不变。
for (let i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
另一种方法是再创建一个闭包:
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
这里利用了之前提到的函数工厂概念,为不同的闭包创建了不同的作用域环境。
还有一种方法是将 item 包含在新的函数作用域中,从而每次创建都是新的 item,这与 let 的原理相似:
for (var i = 0; i < helpText.length; i++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})();
}
第二个常见问题是内存泄漏。
function parentFunction(paramA)
{
var a = paramA;
function childFunction()
{
return a + 2;
}
return childFunction();
}
在上述示例中,childFunction 引用了 parentFunction 的变量 a。只要 childFunction 仍在被使用,a 就无法被释放,导致 parentFunction 无法被垃圾回收。
闭包的性能问题
我们定义了一个对象,并通过闭包访问其私有属性:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
上述对象存在什么问题?
问题在于,对于每个 new 出来的对象,getName 和 getMessage 方法都会被复制一份。这不仅造成内容冗余,还影响性能。
通常,我们将对象的方法定义在 prototype 上:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
注意,不要直接重写整个 prototype,这可能导致未知错误,只需根据需要重写特定方法即可。
总结
闭包是 JavaScript 中非常强大且有用的概念,希望大家能掌握并喜爱它。
到此这篇关于 JavaScript 中闭包 closure 的文章就介绍到这了,更多相关 JavaScript 闭包 closure 内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!
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
