17

In jQuery we have a function each like this

$('button').each(function(i) {
  $('button').eq(i).css('background', 'red');
});

How can we replace this code with plain JavaScript?

Paul Roub
  • 36,322
  • 27
  • 84
  • 93
men3m
  • 221
  • 2
  • 10

5 Answers5

26

DOM selection with querySelectorAll()

Browsers have several ways to select elements from the DOM, but perhaps the most flexible and convenient is querySelectorAll. It lets you use CSS style selectors to grab all matching elements from a given root.

In your case, it would look like this:

document.querySelectorAll("button");

Shortening querySelectorAll()

As nice as that is, it is a little verbose, so it's not uncommon to create a wrapping function that shortens it. That's what we'll do here, giving it the name Q.

function Q(root, selector) {
  if (typeof root === "string") {
    selector = root
    root = document
  }
  return root.querySelectorAll(selector)
}

The first argument is the context from which you're doing the selection, and the second is the selector. If you only pass a string, it'll use document as the context.

So now your DOM selection would be this, which we'll use hereafter:

Q("button");

Borrowing Array.prototype.forEach

A pretty common way to do a functional looping construct is to borrow the forEach method of Array.prototype and call it on the collection of elements by using the function's .call() method like this:

Array.prototype.forEach.call(Q("buttons"), function(el) {
  el.style.background = "red";
});

Or in the most modern browsers, we can use arrow functions to shorten it a little:

Array.prototype.forEach.call(Q("buttons"), el => el.style.background = "red");

Binding and caching the borrowed .forEach()

The .forEach() borrow can be shortened if early in your application, you use the Function prototype's bind() method to bind the .forEach() method to the this value of .call().

const each = Function.call.bind(Array.prototype.forEach);

That way you can just call it like a function that receives the element collection as the first argument.

each(Q("button"), function(el) {
  el.style.background = "red";
});

Or again using an arrow function in some of the newest browsers:

each(Q("button"), el => el.style.background = "red");

Using Array.from()

The Array.from method was also introduced to easily convert array-like objects into actual arrays. This would let you use .forEach() directly, and can be patched into legacy browsers with a simple polyfill (see the docs link).

Array.from(Q("button")).forEach(function(el) {
  el.style.background = "red";
});

If you put the Array.from call directly in our Q function from above, you'll be able to call .forEach() directly.

Q("button").forEach(function(el) {
  el.style.background = "red";
});

Using a for-of loop

In the latest browsers, you can use a for-of loop instead, making everything very short and clean:

for (const el of Q("button")) {
  el.style.background = "red";
}

This way there's no need to convert to an Array or use .forEach.


Transpiling modern code

For the examples above that require the most modern browsers, there are transpilers available (for example Babel) that will translate the latest standards into code that will work in older browsers.


Creating a custom each()

As a side note, if you'd like this to refer to the current element, or have any other specific behavior, here's a basic each implementation that receives the collection and a callback.

function each(a, callback) {
  for (var i = 0; i < a.length; i++) {
    callback.call(a[i], a[i], i, a);
  }
}

Though using this in that way is generally not needed since you already have the element as a parameter.

  • Nice. But in jQuery's `$(...).each()`, `this` would point to the element. – haim770 Nov 28 '16 at 21:13
  • @haim770: That's true, though I think the OP simply wanted to know how to do that kind of loop in JS, and not necessarily have it exactly as it would behave in jQuery. –  Nov 28 '16 at 21:17
  • 1
    @squint Why did you make this a CW? – Praveen Kumar Purushothaman Nov 28 '16 at 21:17
  • 3
    @PraveenKumar: All my answers are that way (or I change them as I come across non-CW ones). I want others to feel comfortable adding information and improvements to the answers I post. –  Nov 28 '16 at 21:19
5

You can use the function getElementsByTagName() which queries the DOM for elements of a certain tag and returns a collection of elements. You can call this function on document or you could be more specific and select an element using document.getElementById() and then look for tags inside of that element.

Either way, once you have a collection of elements, you can then loop over the collection and apply styles accordingly.

//query the document for all <button> elements
var buttons = document.getElementsByTagName('button');

/* -- OR -- */

//query the document for all buttons inside of a specific element
var container = document.getElementById('container');
var buttons = container.getElementsByTagName('button');

//loop over the collection of buttons and set each background to 'red'
for(var i=0; i<buttons.length; i++) {
    buttons[i].style.background = "red";
}

Edit: I realize this is not the JS behind how jQuery's each function works. OP didn't state that he wanted to see that specifically, just a way to accomplish the $.each() functionality with JS (as there are many possibilities). This example is just a trivial approach using a very basic concept.

Mike Hamilton
  • 1,519
  • 16
  • 24
4
var buttons = document.querySelectorAll("button");
for(var x in buttons){
  var e = buttons[x];
  e.innerHTML="stuff";
}

as squint pointed out, it should be buttons[x] and var x. sorry about that.

3

Supported by more browsers since it doesn't use forEach.

If using getElementsByTagName, add the function to HTMLCollection.

HTMLCollection.prototype.each = function(callback){
    for(var i=0; i<this.length; i++) callback(this[i]);
};

document.getElementsByTagName('div').each(function(div){
    div.innerHTML = "poo";
});

If using querySelectorAll you can attach the same function to NodeList

NodeList.prototype.each = function(callback){
    for(var i=0; i<this.length; i++) callback(this[i]);
};

document.querySelectorAll('div').each(function(div){
    div.innerHTML = "podo";
});

To appease the chanters (see comments), I'll specify that if this is for a library you're writing, or if it will be used alongside a library that could possibly overwrite it, obviously you would want to consider other methods.

Fiddle: https://jsfiddle.net/er5amk8j/

I wrestled a bear once.
  • 22,983
  • 19
  • 69
  • 116
  • 1
    [Don't modify objects you don't own.](http://stackoverflow.com/q/6223449/3853934) – Michał Perłakowski Nov 29 '16 at 05:59
  • @Gothdo Don't make blanket statements that only apply in certain contexts and otherwise make no sense at all. – I wrestled a bear once. Nov 29 '16 at 18:45
  • If you don't think that applies in your context, please explain that. – Michał Perłakowski Nov 29 '16 at 18:48
  • i said what i intended to say. it's the same mantra people are always chanting about `eval`. if the functionality didn't have any value or purpose it wouldn't be part of the standard. you (and everyone chanting with you) are doing more harm than good by making blanket statements about what functionality to use and what not to. you're the one making the claim so the burden of explanation is on you.. – I wrestled a bear once. Nov 29 '16 at 19:42
  • 1
    The explanation is in the question I linked: "The problem is that prototype can be modified in several places. For example one library will add map method to Array's prototype and your own code will add the same but with another purpose. So one implementation will be broken." – Michał Perłakowski Nov 29 '16 at 19:44
  • 1
    your original statement was "don't do so and so" it didn't have any exceptions or conditions. without coditions, your statement is inaccurate and unhelpful, and with the appropriate conditions it's barely relevant. – I wrestled a bear once. Nov 29 '16 at 20:18
2

But you are also using $.each() in a dubious way in your example. It is much easier and faster to access elements using this.

$('button').each(function(i) {
  $(this).css('background', 'red');
});

Most simple replacement would be using simple function closures but it's not pretty.

var $buttons = $('button');

for(var i = 0; i < $buttons.length; i++){
  (function(i){
    $buttons.eq(i).css('background', 'red');
  })(i);
}

Or to set this use .call() to invoke function.

var $buttons = $('button');

for(var i = 0; i < $buttons.length; i++){
  (function(i){
    $(this).css('background', 'red');    
  }).call($buttons[i], i);
}
Bizniztime
  • 1,016
  • 10
  • 22
  • The IIFE wouldn't actually be needed in the second example because `i` is used immediately in the loop instead of later in a callback. You're right about the OP's technique though. Calling `$('button')` over and over in the loop will be slow. –  Nov 29 '16 at 13:22
  • @squint You are right. But I've expanded `Michael Hamilton` answer on purpose to show that although results are same it is not proper way to replace `.each()`. – Bizniztime Nov 29 '16 at 14:08