如何在 JavaScript 的循环中添加 eventListeners 而不触发 JSHint 的混淆语义错误? 更好的解决方案展开警告

问题描述

我正在使用 DOM 在 JavaScipt 中创建动态项目列表。每个项目都有一个链接一个 HTML 元素),链接页面上的另一个元素;允许用户编辑团队的表单。链接工作正常,但我试图通过向链接添加一个 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 存在于循环范围之外。但是,我不明白为什么将表放入循环会改变任何事情。我到底应该在代码中更改什么以确保 JSHint 会满意?如果看起来是更好的解决方案,我也可以使用除添加 eventListener 之外的其他方法

干杯!

解决方法

如果我理解正确,这是因为变量 table 存在于循环范围之外。

link,而不是 table

我到底应该在代码中更改哪些内容以确保 JSHint 会满意?

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

IIRC,JSHint 比它最初基于的 JSLint 的可配置性要高得多(几年前的现在),您可以查看是否有选项可以在您使用 let 时关闭此特定警告link

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

但是您可能会更改 team 所指对象的 link 属性的可能性很小。如果你想消除这种可能性并摆脱显式函数,你可以使用 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",team));
    });                                    // 
    link.textContent = team.name;
    //code that adds the link and other elements into the cells of the table
}
,

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

与旧版本的 linter 一样,我认为在循环的每次迭代中 [重新] 定义相同的函数在任何时候都是不好的做法。因为函数声明发生在循环中,所以每个单独函数的逻辑与其他函数的逻辑相同。这意味着代码不能是 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 上的那些 lint。

此代码使用工厂维护原始闭包,并在每次循环迭代中返回一个作用域包装函数。

很好,它清楚地表明受保护的范围是有意的——你希望每次迭代都有一个新函数,因为范围很重要——并且还限制了其中的内容受保护的范围为 link 而没有别的,但除此之外没有任何好处。

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

(我们指望 variable hoistinglink 传递给我们的函数应该是您需要知道的所有 code smell 可能有更好的解决方案。)


更好的解决方案

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

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

我很确定您可以从 e.target 获得您想要的参考(除非您支持 IE 8 or lower)。然后我们根本不需要传递 link。我们可以从我们的事件中推导出它。

/*jshint esversion: 6 */
/*global updateForm,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 properties 是在对象初始化后砸到 DOM 元素上的非标准元素。

这些属性可以在没有警告的情况下被其他库相当普遍地编辑和/或删除。您可能最好使用 data attributes...

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

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

有意义吗?