Javascript事件侦听器结构



我正在编写一个代码,以获取单击的元素的索引,这样它就可以添加或删除一个类来显示或隐藏信息。为此,我使用for进行迭代。但我不明白为什么在事件处理程序后面有一个(I)。我是一个编码新手,所以我想了解一切。

这是JavaScript代码:

for (let i = 0; i < questions.length; i++) {
questions[i].addEventListener(‘click’,((e) => {
return function() {
if (clic[e].classList.contains(‘q-answered)) {
clic[e].classList.replace(‘q-answered’, ‘q-answeredno’);
} else if (clic[e].classList.contains(‘q-answeredno’)) {
clic[e].classList.replace(‘q-answeredno’, ‘q-answered’);
}
}
})(i))
}

让我们从使用var迭代questions的情况开始

简单地说,它使它成为一个立即调用的函数表达式(简称IIFE),并传递一个您通常无法访问的参数

当单击事件处理程序回调需要一个带有单个变量的函数时。当事件被处理时,函数会被调用,JS运行时会提供一个指针事件,返回到您的函数进行处理。这一切都很好,但在这里,您想知道数组中单击的元素的偏移量,该偏移量是从该回调范围外收集的信息中获得的。

因此,您改为更改回调形状。您传入自己的函数并将其封装在括号中。在JS中,您可以在定义后面的括号中向函数传递参数。您的示例使用lambda语法,因此从函数本身开始:

(e) => {return ...;}

但是,如果这就是这里传递的全部内容,那么您将获得一个PointerEvent,因为它与预期的回调形状相匹配。因此,您需要将其封装在括号中:

((e) => {return ...;})

很好,但是您希望这个函数在执行时传递一个非常特殊的值,所以您在最后定义参数。您使用i作为标识偏移元素索引的变量,所以我们将在这里传递它。

((e) => { return ...; })(i)

现在,这意味着当为第一个元素调用事件处理程序函数时,它实际上看起来如下:

((e) => {return ...; })(0); //Zero for the first zero-based index

这排除了事件处理程序回调将自己的值分配给第一个变量的可能性,这意味着现在将调用该函数,您将把0(或以其他方式设置索引属性值)传递给e参数,返回语句的其余部分将相应地执行。

我听说过这个闭包是什么?它在这里可能如何应用

@t.niese在我最初错过的关于闭包的评论中提出了一个很好的观点,以及为什么它们在这里非常相关。

简单地说,在JavaScript中,您可以引用函数范围内定义的变量、调用函数(例如父函数)范围的变量或全局范围上的任何变量。闭包是任何能够保留对这些变量的引用的函数,而不管父函数是否已经返回。

如果我有以下内容:

function listFruits(fruits) {
var suffix = "s";

for (var a = 0; a < fruits.length; a++) {
console.log(`I like ${fruits[a]}${suffix}`);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"

正如您所期望的,尽管在循环之外分配suffix,但我可以在循环中使用它来使每个水果名称具有复数形式。但我也可以使用一个内部函数:

function listFruits(fruits) {
var suffix = "s";

function pluralizeName(name) {
return `${name}${suffix}`;
}

for (var a = 0; a < fruits.length; a++) {
var pluralName = pluralizeName(fruits[a]);
console.log(`I like ${pluralName}`);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"

同样,尽管suffix是在pluralizeName函数的父函数中分配的,但我仍然可以在返回字符串中分配它,以便在循环中登录到控制台。

所以,让我们在那里放一个超时回调,看看会发生什么:

function listFruits(fruits) {
var suffix = "s";

for (var a = 0; a < fruits.length; a++) {
setTimeout(function() {
console.log(`I like ${fruits[a]}${suffix}`);
}, 1000);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like undefineds"
// >> "I like undefineds"
// >> "I like undefineds"

为什么我们没有像以前一样在这里列出我们喜欢的水果?a仍在父级中定义,并且它确实按照应该的方式附加了suffix值,那么给出了什么呢?

好吧,我们的循环从a=0开始,将超时设置为以1000ms执行回调,然后将a递增为1,将超时设为以1000m执行回调,再将a递增为2,将超时值设为以1000毫秒执行回调,随后将a递增为3,将超时设定为以1000 ms执行回调,则将CCD_ 14增加到4,此时点CCD_。但是当超时回调运行时,a现在的值为4,而fruits[4]是未定义的,所以我们在控制台日志中看到了这一点。

好吧,但我们希望能够在本地引用迭代索引的值,这样第一个回调对第一个对象有效,第二个对第二个对象有效等等,那么我们如何使它对回调可用呢?我们使用上述IIFE方法。

function listFruits(fruits) {
var suffix = "s";

for (var a = 0; a < fruits.length; a++) {
(function() {
var current = a;

setTimeout( function() {
console.log(`I like ${fruits[current]}${suffix}`);
}, 1000);
})();
}
}
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"

它在这里工作是因为我们为循环创建的每个函数创建了一个新的闭包。创建函数时,我们从父级获取a的当前值,并将其分配给一个局部变量,这样当setTimeout方法稍后激发时,它将使用该局部current值来正确访问fruits数组的预期索引。

但是,我可以将a变量作为参数传递到IIFE,而不是像上面在var current = a;中那样捕获变量,它将具有完全相同的效果:

function listFruits(fruits) {
var suffix = "s";

for (var a = 0; a < fruits.length; a++) {
(function(current) {

setTimeout( function() {
console.log(`I like ${fruits[current]}${suffix}`);
}, 1000);
})(a);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"

我们的IIFE用传入的参数a填充current变量,使其在本地可用,我们得到了预期的结果。

但是我的示例使用let,那么这会改变什么呢

在ES6之前,我们只有全局和函数作用域需要使用IFFE来引入本地函数作用域,如上所述。有了ES6,我们得到了一个新的";块范围";它基本上对两个大括号内的所有内容进行范围划分,包括其中的任何数量的子块,但前提是使用constlet关键字分配变量。var仍然只分配给全局或函数范围。

让我们回顾一下上面的例子,在这个例子中,我们收到了所有未定义的值,并用let替换了对var的使用。

function listFruits(fruits) {
var suffix = "s";

for (let a = 0; a < fruits.length; a++) { //Note the change to 'let' here
setTimeout(function() {
console.log(`I like ${fruits[a]}${suffix}`);
}, 1000);
}
}
listFruits(['grape', 'apple', 'orange']);
// >> "I like grapes"
// >> "I like apples"
// >> "I like oranges"

这一次它起作用是因为a的值仍然是分配给setTimeout回调的块作用域的。

好的,那么我的样品呢

让我们把它带回一个完整的圆圈。如果您使用的是var,那么您的示例将i的当前值确定为IIFE,这样您的每个事件处理程序回调都可以访问适当的偏移节点。

但是,由于您使用的是let,使用IFFE肯定不会造成任何损害,但这是不必要的,因为i的预期值可通过回调函数的块作用域使用。因此,您可以简化以删除IIFE,并且不承担任何后果。

for (let i = 0; i < questions.length; i++) {
questions[i].addEventListener(‘click’, function() {
if (clic[i].classList.contains(‘q-answered)) {
clic[i].classList.replace(‘q-answered’, ‘q-answeredno’);
} else if (clic[i].classList.contains(‘q-answeredno’)) {
clic[i].classList.replace(‘q-answeredno’, ‘q-answered’);
}
});
}

如果你对此有任何疑问,请留下评论,我很乐意编辑以解决。

最新更新