如何在 JavaScript 的循环中添加 eventListeners 而不会触发 JSHint 的混淆语义错误?

How to add eventListeners within a loop in JavaScript without triggering JSHint's confusing semantics error?

我正在使用 DOM 在 JavaScipt 中创建项目的动态列表。每个项目都有一个 link(一个 HTML 元素),link 指向页面上的另一个元素;允许用户编辑团队的表单。 link 工作正常,但我试图通过向 link 添加 eventListener 来使表单工作,因为表单需要获取我们正在编辑的团队作为参考。

这是有问题的代码:

    for (let i = 0; i < teams.length; i++) {
    let row = table.insertRow(-1);
    //removed code that made cells under the row
    let link = document.createElement("a");
    link.setAttribute("href","#form");
    link.team = teams[i];
    link.addEventListener("click", function (e) {
        updateForm(link.team);
    });
    link.textContent = teams[i].name;
    //code that adds the link and other elements into the cells of the table
}

为了清楚起见,我删除了部分代码。重要的是 addEventListener,它功能完美。当用户单击按钮时,当前的方式是正确发送有问题的团队。但是,我从 JSHint 收到以下消息:

(error) Functions declared within loops referencing an outer scoped variable may lead to confusing semantics.

如果我没理解错的话,这是因为变量 table 存在于循环范围之外。但是,我不明白为什么将 table 放入循环内部会改变任何事情。我究竟应该在代码中更改什么以确保 JSHint 会满意?如果看起来是更好的解决方案,我也可以使用除添加 eventListener 之外的其他方法。

干杯!

If I've understood correctly, this is because the variable tableexists outside of the scope of the loop.

link,不是table

What exactly am I supposed to change in the code to make sure that JSHint would be happy?

我从另一边来:你需要在 JSHint 中更改什么/你的 linting 设置,以便你在回调中使用 link 的完全有效的代码不会触发 linting错误?如果您的代码使用 var 警告将非常有用,但您的代码使用 let.

IIRC,JSHint 比它最初基于的 JSLint(几年前现在)更可配置,您可能会看看是否有一个选项可以在您使用 let link.

或者,您可能会研究另一种 linting 解决方案(ESLint 非常流行)。

但是您可能会更改 link 所指对象的 team 属性 的可能性很小。如果你想消除这种可能性并摆脱显式功能,你可以使用 bind:

link.addEventListener("click", updateForm.bind(null, link.team));

这也将消除 JSHint 警告。

或者由于您在元素本身上使用了 expando 属性,您可以使用 this:

link.addEventListener("click", function() {
    updateForm(this.team);
});

...但我建议您 不要 在 DOM 元素上使用 expando 属性。例如:

for (const team of teams) {
    const row = table.insertRow(-1);
    //removed code that made cells under the row
    const link = document.createElement("a");
    link.setAttribute("href","#form");
    link.addEventListener("click", () => { // Or:
        updateForm(team);                  //     link.addEventListener("click", updateForm.bind(null, team));
    });                                    // 
    link.textContent = team.name;
    //code that adds the link and other elements into the cells of the table
}

每当您在循环中定义函数时,Linters 都会抱怨 b/c 您不是 DRY。 (我有点惊讶 JSHint 没有,但 JSLint 似乎也不再认为这是一个错误!)

与旧版本的 linters 一样,我认为在循环的每次迭代中[重新]定义相同的函数在任何时候都是一种不好的做法。因为函数声明发生在循环中,所以每个单独的函数的逻辑与其他函数的逻辑相同。这意味着代码不能是 DRY 的。它还允许像您一样进行复杂的范围设置!

link.addEventListener("click", function (e) {
    updateForm(link.team);
});    

通常您应该将该函数从循环中拉出,并在循环外定义对它的单个引用。

但是... 你通过后门将变量传递到你的函数中,有一些,嗯,创造性的闭包使用,这是一个更细微的错误 [linters 显然仍然关心]。

这是一种修复 lint 的方法,它使初始代码中范围界定的重要性变得明确(尽管请继续阅读!)。

/*jshint esversion: 6 */
/*global updateForm, teams, table */

// Note that this function returns a function containing a closure
// that maintains one `link` per call.
function myListenerFactory(link) {
    return function (e) {
        updateForm(link.team);
    };
}

for (let i = 0; i < teams.length; i++) {
    let row = table.insertRow(-1);
  
    //removed code that made cells under the row
  
    let link = document.createElement("a");
    link.setAttribute("href","#form");
    link.team = teams[i];
    
    // Use the same function each time. Note that it's called IMMEDIATELY,
    // returning a new function for each iteration.
    link.addEventListener("click", myListenerFactory(link)()); // <<< Same function reused in the loop
    
    link.textContent = teams[i].name;
    
    //code that adds the link and other elements into the cells of the table
}

在 jshint.com 上 lints。

此代码通过使用工厂和每个循环迭代 returns 一个作用域包装函数来维护原始代码的闭包。

很好,因为它清楚地表明受保护范围是有意的 -- 您想要每次迭代一个新函数,因为范围很重要-- 并且还将受保护范围内的内容限制为 link,除此之外没有其他任何好处。

这就是为什么 TJ 说“您的代码在回调中使用 link 完全有效”——您的代码取决于闭包,因此从技术上讲,每个重新定义都是不同的,尽管只是其范围不同。它从一开始就是干的。它只是看起来不像。它看起来不像它的作用,imo。

(我们指望 variable hoisting to pass link to our function should be all the code smell 你需要知道可能有更好的解决方案。)


更好的解决方案

所以我想稍微强调一下“完全有效”,我认为 TJ 也是这样做的,他说要避免 expando 属性。这不是最容易理解的代码(上面的 lint 传递替代方案也不是),而这正是 linter 所识别的。

让我们尝试找到更好的选择。

我很确定您可以从 e.target 中获得您想要的参考(除非您支持 IE 8 or lower)。那么我们根本不必绕过 link 。我们可以从我们的事件中得出它。

/*jshint esversion: 6 */
/*global updateForm, teams, table */

function myEventHandler(e) {
    // No need to backdoor `link` when we can derive it from `e`!
    updateForm(e.target.team);
}

for (let i = 0; i < teams.length; i++) {
    let row = table.insertRow(-1);
  
    //removed code that made cells under the row
  
    let link = document.createElement("a");
    link.setAttribute("href","#form");
    link.team = teams[i];
    
    // Use the same function each time
    link.addEventListener("click", myEventHandler);
    
    link.textContent = teams[i].name;
    
    //code that adds the link and other elements into the cells of the table
}

繁荣。没有古怪的作用域,没有重复的函数声明。完美。


扩展警告

同样,TJ 关于避免“expando 属性”的警告非常好。 Expando properties 是非标准的,你在对象初始化后粉碎到 DOM 元素上。

这些属性通常可以被其他库编辑 and/or 删除而不发出警告。你最好使用 data attributes...

... 或者,我的偏好,通过向每个 link 添加唯一元素 ID 来访问查找 table。

也就是说,我不喜欢 TJ 的解决方案,即让您保持最初的范围界定依赖时髦;^D,但它也有效。

有道理吗?