在编写原生 JavaScript 时,一个常见的需求是在 DOM 中找到一组元素并循环遍历它们。 例如,找到按钮实例并向它们附加点击处理程序。
const buttons = document.querySelectorAll(".js-do-thing");
// There could be any number of these!
// I need to loop over them and attach a click handler.
有非常多种方法可以实现这一点。 让我们逐一查看。
forEach
forEach
通常用于数组,有趣的是,querySelectorAll
返回的不是数组,而是 NodeList。 幸运的是,大多数现代浏览器都支持在 NodeList 上使用 forEach
。
buttons.forEach((button) => {
button.addEventListener('click', () => {
console.log("forEach worked");
});
});
如果您担心 forEach
可能无法在您的 NodeList 上运行,可以先将其扩展为数组
[...buttons].forEach((button) => {
button.addEventListener('click', () => {
console.log("spread forEach worked");
});
});
但我其实不确定这样做是否有帮助,因为似乎不太可能存在支持扩展但 NodeList 上不支持 forEach
的浏览器。 不过,在使用转译时,情况可能会变得奇怪,但我不知道。 无论如何,如果想使用其他特定于数组的函数,例如 .map()
、.filter()
或 .reduce()
,扩展会比较方便。
一个稍微旧一点的方法是使用这个小技巧接入数组的自然 forEach
[].forEach.call(buttons, (button) => {
button.addEventListener('click', () => {
console.log("array forEach worked");
});
});
Todd Motto 曾经 严厉批评过这种方法,所以请注意。 他建议构建您自己的方法(更新为 ES6)
const forEach = (array, callback, scope) => {
for (var i = 0; i < array.length; i++) {
callback.call(scope, i, array[i]);
}
};
…我们将其用作
forEach(buttons, (index, button) => {
console.log("our own function worked");
});
for .. of
浏览器支持 for for .. of
循环看起来相当不错,对我来说,这似乎是一个超级简洁的语法
for (const button of buttons) {
button.addEventListener('click', () => {
console.log("for .. of worked");
});
}
立即创建一个数组
const buttons = Array.prototype.slice.apply(
document.querySelectorAll(".js-do-thing")
);
现在,您可以使用所有正常的数组函数。
buttons.forEach((button) => {
console.log("apply worked");
});
旧的 for 循环
如果您需要尽可能大的浏览器支持,使用古老的经典 for
循环并不会令人感到羞愧
for (let i = 0; i < buttons.length; ++i) {
buttons[i].addEventListener('click', () => {
console.log("for loop worked");
});
}
等等! 上面的示例包含箭头函数和 ES6 let。 如果您想更旧一些,并支持旧版 IE 等,您需要…
for (var i = 0; i < buttons.length; ++i) {
buttons[i].addEventListener('click', function() {
console.log("for loop worked");
});
}
库
如果您使用的是 jQuery,您甚至不必费心….
$(".buttons").on("click", () => {
console.log("jQuery works");
});
如果您使用的是 React/JSX 设置,您无需考虑这种绑定。
Lodash 也具有 _.forEach
,这可能有助于支持旧版浏览器。
_.forEach(buttons, (button, key) => {
console.log("lodash worked");
});
投票
Twitter 用户
const els = document.querySelectorAll(".foo");
// 您使用哪个循环? 其中一个? 还是其他?
— Chris Coyier (@chriscoyier) 2018 年 11 月 7 日
此外,这里有一个示例,其中包含所有这些选项。
比 NodeList.forEach 的支持略好(虽然仍然没有 IE,如果您需要的话)
您是否知道可以将映射函数传递给
Array.from
? 您可以使用它来迭代值。我经常在
Array.from(buttons)
之后使用它,这样我就可以获得一个正常的array
- 这里有更多信息以及一个 polyfill: https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from[…document.querySelectorAll(‘.my-item’)].forEach((item) => {item.doSomething()});
ES6 还引入了 Array.from 方法。 您可以为其提供类似数组的对象(例如 NodeList)
[].forEach.call
效率低下,不必要地实例化了一个数组; 而是使用Array.prototype.forEach.call
。 为了进一步提高效率和人体工程学,请使用const forEach = Array.prototype.forEach;
和forEach.call(…)
。 如果你不喜欢使用.call
,const forEach = Function.prototype.call.bind(Array.prototype.forEach);
就可以让你只用forEach(…)
。但实际上,在几乎所有情况下,我强烈建议在必要时仅对库功能进行 polyfill,选择一个合适的基线,这样只有很少的用户需要 polyfill,而旧版浏览器可以轻松得到支持。 我发现
Object.values
是一个很好的功能,可以用来检查语言库功能。 对于 NodeList.prototype.forEach(不是语言本身的库功能),您可以直接使用if (!('forEach' in NodeList.prototype) { NodeList.prototype.forEach = Array.prototype.forEach; }
。令我惊讶的是,这里没有提到在列表中的每个节点上附加事件处理程序的性能影响。 嗯,大多数时候可能不会有问题,但有时可能会出现问题,而更好的方法是在父节点上放置一个事件处理程序,然后通过 Event.target 对相关节点进行操作。
更经济的循环方式可以基于 while 循环,这样代码的执行速度可以快 50%,因为在每次迭代中,它只是从“i”中减去一个值,该值大于“0”,所以它不是 falsy,所以循环会继续执行; 然后,可以通过隐藏已找到节点函数来优化逻辑,该函数使用 callback 和 anonyme 函数。 这个想法来自 Stoyan Stefanov 的 ‘JavaScript 模式’ 中的回调模式的第 4 章
不错的选择。
别忘了 Object.values()
我喜欢使用老式方法,只是为了支持尽可能多的浏览器,但是如果要在循环中执行复杂的操作,我会使用闭包来进行作用域。
委托事件处理程序通常也很值得考虑。
它们不需要等待整个页面下载和解析。 相反,您可以将事件处理程序附加到文档上。 这意味着您可以避免页面上那些虽然可见但直到 JS 到达后才工作的元素的问题(假设 JS 会到达…)。
它们也不需要通过 querySelector 进行同步 DOM 遍历。 它们不需要附加很多事件处理程序。
https://gomakethings.com/why-event-delegation-is-a-better-way-to-listen-for-events-in-vanilla-js/