闭包 - JavaScript

xwbar的头像
2026-01-05 10:50:40
/
世界杯克罗地亚

在循环中创建闭包:一个常见错误

在引入 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();

鼻和鼻窦
问道比翼鸟的属性如何?选择比翼鸟有什么好处?