闭包 - JavaScript
在循环中创建闭包:一个常见错误
在引入 let 关键字之前,当你在循环中创建闭包时,会发生一个常见的闭包问题,注意下面的代码示例:
html
Helpful notes will appear here
Email:
Name:
Age:
jsfunction showHelp(help) {
document.getElementById("help").textContent = help;
}
function setupHelp() {
var helpText = [
{ id: "email", help: "Your email 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`
var item = helpText[i];
document.getElementById(item.id).onfocus = function () {
showHelp(item.help);
};
}
}
setupHelp();
试着在 JSFiddle 中运行该代码。
helpText 数组中定义了三个有用的提示信息,每个都与文档中 input 字段的 ID 关联。循环遍历这些定义,将 onfocus 事件与显示帮助信息的方法进行关联。
如果你试着运行这段代码,你会发现它没有达到预期的效果。无论你聚焦在那个字段上,显示的都是关于年龄的信息。
原因是赋值给 onfocus 的函数创建了闭包。这些闭包是由函数定义和从 setupHelp 函数作用域中捕获的环境所组成的。这三个闭包在循环中创建,但每个都共享同一个词法环境,这个环境有一个不断改变值的变量(item)。这是因为 item 变量用 var 声明,并由于声明提升,因此拥有函数作用域。而 item.help 的值是在 onfocus 回调执行时决定。因为循环在事件触发之前早已执行完毕,所以 item 变量对象(由三个闭包共享)已经指向了 helpText 的最后一项。
这个例子的一个解决方案就是使用更多的闭包:特别是使用前面所述的函数工厂:
jsfunction showHelp(help) {
document.getElementById("help").textContent = help;
}
function makeHelpCallback(help) {
return function () {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{ id: "email", help: "Your email 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 = makeHelpCallback(item.help);
}
}
setupHelp();
使用这个 JSFiddle 链接运行该代码。
这次符合预期。回调不再都共享同一个词法环境,makeHelpCallback 函数为每一个回调创建了一个新的词法环境,在每个新的词法环境中,help 指向 helpText 数组中对应的字符串。
另一种方法是使用匿名闭包:
jsfunction showHelp(help) {
document.getElementById("help").textContent = help;
}
function setupHelp() {
var helpText = [
{ id: "email", help: "Your email 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++) {
(function () {
var item = helpText[i];
document.getElementById(item.id).onfocus = function () {
showHelp(item.help);
};
})(); // 立即将事件监听器附着到当前的 item 值(保留到每次迭代)。
}
}
setupHelp();
如果你不想使用过多的闭包,你可以使用 let 或 const 关键词:
jsfunction showHelp(help) {
document.getElementById("help").textContent = help;
}
function setupHelp() {
const helpText = [
{ id: "email", help: "Your email address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
for (let i = 0; i < helpText.length; i++) {
const item = helpText[i];
document.getElementById(item.id).onfocus = () => {
showHelp(item.help);
};
}
}
setupHelp();
这个示例使用 const 而不是 var,因此每个闭包绑定的是块作用域变量,这意味着不再需要额外的闭包。
另一个可选方案是使用 forEach() 遍历 helpText 数组并给每一个 添加一个监听器,如下所示:
jsfunction showHelp(help) {
document.getElementById("help").textContent = help;
}
function setupHelp() {
var helpText = [
{ id: "email", help: "Your email address" },
{ id: "name", help: "Your full name" },
{ id: "age", help: "Your age (you must be over 16)" },
];
helpText.forEach(function (text) {
document.getElementById(text.id).onfocus = function () {
showHelp(text.help);
};
});
}
setupHelp();