|
|
|
|
|
在循環(huán)中創(chuàng)建閉包,很容易出現(xiàn)問(wèn)題,本文將通過(guò)5個(gè)示例,介紹在循環(huán)中創(chuàng)建閉包的常見(jiàn)錯(cuò)誤,以及如何使用正確的方法。
在循環(huán)中創(chuàng)建閉包的常見(jiàn)錯(cuò)誤
在循環(huán)中有一個(gè)常見(jiàn)的閉包創(chuàng)建問(wèn)題,我們先來(lái)看看例子。
完整HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<p id="help">點(diǎn)擊輸入框時(shí),這里顯示提示信息</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
<script type="text/javascript">
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': '你的Email'},
{'id': 'name', 'help': '你的名字'},
{'id': 'age', 'help': '你的年齡'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
</script>
</body>
</html>
示例的預(yù)期是,當(dāng)點(diǎn)擊輸入框時(shí),顯示對(duì)應(yīng)的提示信息。
不過(guò)執(zhí)行結(jié)果卻不符合預(yù)期,點(diǎn)擊輸入框時(shí),提示信息并不跟著變化。無(wú)論焦點(diǎn)在哪個(gè)input
上,顯示的都是關(guān)于年齡的信息。
出現(xiàn)這個(gè)原因是在循環(huán)中使用的閉包有問(wèn)題。
數(shù)組 helpText
中定義了三個(gè)有用的提示信息,每一個(gè)都關(guān)聯(lián)于對(duì)應(yīng)的文檔中的 input
的 ID。通過(guò)循環(huán)這三項(xiàng)定義,依次為相應(yīng)input
添加了一個(gè) onfocus
事件處理函數(shù),以便顯示幫助信息。
運(yùn)行這段代碼后,你會(huì)發(fā)現(xiàn)它沒(méi)有達(dá)到想要的效果。無(wú)論焦點(diǎn)在哪個(gè)input
上,顯示的都是關(guān)于年齡的信息。
原因是賦值給 onfocus
的是閉包。這些閉包是由他們的函數(shù)定義和在 setupHelp
作用域中捕獲的環(huán)境所組成的。這三個(gè)閉包在循環(huán)中被創(chuàng)建,但他們共享了同一個(gè)詞法作用域,在這個(gè)作用域中存在一個(gè)變量 item
。這是因?yàn)樽兞?code>item使用 var
進(jìn)行聲明,由于變量提升,所以具有函數(shù)作用域。當(dāng)onfocus
的回調(diào)執(zhí)行時(shí),item.help
的值被決定。由于循環(huán)在事件觸發(fā)之前早已執(zhí)行完畢,變量對(duì)象item
(被三個(gè)閉包所共享)已經(jīng)指向了helpText
的最后一項(xiàng)。
我們可以通過(guò)幾種方法來(lái)解決這個(gè)問(wèn)題。
解決方法一:使用更多的閉包
解決這個(gè)問(wèn)題的一種方案是使用更多的閉包:特別是使用前面所述的函數(shù)工廠。
完整HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<p id="help">點(diǎn)擊輸入框時(shí),這里顯示提示信息</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
<script type="text/javascript">
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': '你的Email'},
{'id': 'name', 'help': '你的名字'},
{'id': 'age', 'help': '你的年齡'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();
</script>
</body>
</html>
運(yùn)行結(jié)果
這段代碼可以如我們所期望的那樣工作。所有的回調(diào)不再共享同一個(gè)環(huán)境, makeHelpCallback
函數(shù)為每一個(gè)回調(diào)創(chuàng)建一個(gè)新的詞法環(huán)境。在這些環(huán)境中,help
指向 helpText
數(shù)組中對(duì)應(yīng)的字符串。
解決方法二:使用匿名閉包
另一種方法是使用匿名閉包。
完整HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<p id="help">點(diǎn)擊輸入框時(shí),這里顯示提示信息</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
<script type="text/javascript">
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': '你的Email'},
{'id': 'name', 'help': '你的名字'},
{'id': 'age', 'help': '你的年齡'}
];
for (var i = 0; i < helpText.length; i++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // 馬上把當(dāng)前循環(huán)項(xiàng)的 item 與事件回調(diào)相關(guān)聯(lián)起來(lái)
}
}
setupHelp();
</script>
</body>
</html>
執(zhí)行結(jié)果
解決方法三:使用let關(guān)鍵詞
如果不想使用過(guò)多的閉包,你可以用 ES2015 引入的 let
關(guān)鍵詞,在for
循環(huán)內(nèi)把var
改為let
。參考文章:
完整HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<p id="help">點(diǎn)擊輸入框時(shí),這里顯示提示信息</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
<script type="text/javascript">
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': '你的Email'},
{'id': 'name', 'help': '你的名字'},
{'id': 'age', 'help': '你的年齡'}
];
for (var i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
</script>
</body>
</html>
執(zhí)行結(jié)果同樣是如預(yù)期的。這個(gè)例子使用let
而不是var
,因此每個(gè)閉包都綁定了塊作用域的變量,這意味著不再需要額外的閉包。
解決方法四:使用forEach遍歷
另一個(gè)可選方案是使用 forEach()
來(lái)遍歷helpText
數(shù)組并給每一個(gè)<p>
添加一個(gè)監(jiān)聽(tīng)器,如下所示:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
<p id="help">點(diǎn)擊輸入框時(shí),這里顯示提示信息</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
<script type="text/javascript">
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': '你的Email'},
{'id': 'name', 'help': '你的名字'},
{'id': 'age', 'help': '你的年齡'}
];
helpText.forEach(function(text) {
document.getElementById(text.id).onfocus = function() {
showHelp(text.help);
}
});
}
setupHelp();
</script>
</body>
</html>
總結(jié)
本文通過(guò)5個(gè)示例,介紹了在循環(huán)中創(chuàng)建閉包的常見(jiàn)錯(cuò)誤,以及如何使用正確的方法。
相關(guān)文章