A common need when writing vanilla JavaScript is to find a selection of elements in the DOM and loop over them. For example, finding instances of a button and attaching a click handler to them.
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.
There are SO MANY ways to go about it. Let’s go through them.
forEach
forEach
is normally for arrays, and interestingly, what comes back from querySelectorAll
is not an array but a NodeList. Fortunately, most modern browsers support using forEach
on NodeLists anyway.
buttons.forEach((button) => {
button.addEventListener('click', () => {
console.log("forEach worked");
});
});
If you’re worried that forEach
might not work on your NodeList, you could spread it into an array first:
[...buttons].forEach((button) => {
button.addEventListener('click', () => {
console.log("spread forEach worked");
});
});
But I’m not actually sure if that helps anything since it seems a bit unlikely there are browsers that support spreads but not forEach
on NodeLists. Maybe it gets weird when transpiling gets involved, though I dunno. Either way, spreading is nice in case you want to use anything else array-specific, like .map()
, .filter()
, or .reduce()
.
A slightly older method is to jack into the array’s natural forEach
with this little hack:
[].forEach.call(buttons, (button) => {
button.addEventListener('click', () => {
console.log("array forEach worked");
});
});
Todd Motto once called out this method pretty hard though, so be advised. He recommended building your own method (updated for ES6):
const forEach = (array, callback, scope) => {
for (var i = 0; i < array.length; i++) {
callback.call(scope, i, array[i]);
}
};
…which we would use like this:
forEach(buttons, (index, button) => {
console.log("our own function worked");
});
for .. of
Browser support for for .. of
loops looks pretty good and this seems like a super clean syntax to me:
for (const button of buttons) {
button.addEventListener('click', () => {
console.log("for .. of worked");
});
}
Make an array right away
const buttons = Array.prototype.slice.apply(
document.querySelectorAll(".js-do-thing")
);
Now you can use all the normal array functions.
buttons.forEach((button) => {
console.log("apply worked");
});
Old for loop
If you need maximum possible browser support, there is no shame in an ancient classic for
loop:
for (let i = 0; i < buttons.length; ++i) {
buttons[i].addEventListener('click', () => {
console.log("for loop worked");
});
}
Wait! That example above has arrow functions and ES6 let. If you’re trying to go older and support old IE and such, you’ll have to…
for (var i = 0; i < buttons.length; ++i) {
buttons[i].addEventListener('click', function() {
console.log("for loop worked");
});
}
Libraries
If you’re using jQuery, you don’t even have to bother….
$(".buttons").on("click", () => {
console.log("jQuery works");
});
If you’re using a React/JSX setup, you don’t need think about this kind of binding at all.
Lodash has a _.forEach
as well, which presumably helps with older browsers.
_.forEach(buttons, (button, key) => {
console.log("lodash worked");
});
Poll
Twitter peeps:
const els = document.querySelectorAll(".foo");
// which loop do you use? one of these? other?
— Chris Coyier (@chriscoyier) November 7, 2018
Also here’s a Pen with all these options in it.
Slightly better support than NodeList.forEach (though still no IE is you need it) is:
Did you know that you can pass a mapping function to
Array.from
? You can use that to iterate over the values.I often use
Array.from(buttons)
after which I have a normalarray
– look her for more info and also a polyfill: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from[…document.querySelectorAll(‘.my-item’)].forEach((item) => {item.doSomething()});
ES6 also introduced Array.from method. You can give it an array-like object (such as a NodeList)
[].forEach.call
is unnecessarily inefficient, instantiating an array for no purpose; useArray.prototype.forEach.call
instead. For a slight further increase in efficiency and probably ergonomics,const forEach = Array.prototype.forEach;
andforEach.call(…)
. If you don’t like needing.call
,const forEach = Function.prototype.call.bind(Array.prototype.forEach);
would let you do justforEach(…)
.)But really, in almost all situations I’d strongly recommend just polyfilling library features where necessary, selecting a decent baseline so that very few users will need the polyfill, but that old browsers can easily be supported. I found
Object.values
to be a good feature to check for when talking language library features. For NodeList.prototype.forEach (a library feature not of the language itself), you can readily just doif (!('forEach' in NodeList.prototype) { NodeList.prototype.forEach = Array.prototype.forEach; }
.I’m surprised there’s no mention of the performance impact from having an event handler attached to every node in the list. Well, most of the time it might not be an issue, but sometimes it could, and then the better way to do it is just to put one event handler on the parent and take actions on the relevant node through Event.target.
Less cost way to loop could be based on : while loop, so the code will execute 50% faster, because on every iteration it simply subtracts a value from “i”, which more than “0”, so it is not falsy, so the loop goes simply on;Than tha logic could be optimized with hiding found nodes function thanks to callback and anonyme functions. The idea is from the 4th Chapter of Callback Pattern from ‘JavaScript Patterns’ of Stoyan Stefanov
Good options.
Don’t forget about Object.values()
I like to go old school just to support as far bar as possible, but i use a closure for scoping if im going to be doing complicated things within the loop.
Delegate event handlers are often worth considering as well.
They don’t require waiting for the full page to download and parse. Instead you can attach the event handler to the document. This means you avoid the problem of elements on the page that are visible but don’t work yet until JS arrives (assuming it will…).
They also don’t require synchronous DOM traversal through querySelector. They don’t require attaching a lot of event handlers.
https://gomakethings.com/why-event-delegation-is-a-better-way-to-listen-for-events-in-vanilla-js/