What does [].forEach.call() do in JavaScript?
Asked Answered
A

14

164

I was looking at some snippets of code, and I found multiple elements calling a function over a node list with a forEach applied to an empty array.

For example I have something like:

[].forEach.call( document.querySelectorAll('a'), function(el) {
   // whatever with the current node
});

but I can't understand how it works. Can anyone explain me the behaviour of the empty array in front of the forEach and how the call works?

Aloysia answered 17/4, 2013 at 6:48 Comment(0)
M
244

[] is an array.
This array isn't used at all.

It's being put on the page, because using an array gives you access to array prototypes, like .forEach.

This is just faster than typing Array.prototype.forEach.call(...);

Next, forEach is a function which takes a function as an input...

[1,2,3].forEach(function (num) { console.log(num); });

...and for each element in this (where this is array-like, in that it has a length and you can access its parts like this[1]) it will pass three things:

  1. the element in the array
  2. the index of the element (third element would pass 2)
  3. a reference to the array

Lastly, .call is a prototype which functions have (it's a function which gets called on other functions).
.call will take its first argument and replace this inside of the regular function with whatever you passed call, as the first argument (undefined or null will use window in everyday JS, or will be whatever you passed, if in "strict-mode"). The rest of the arguments will be passed to the original function.

[1, 2, 3].forEach.call(["a", "b", "c"], function (item, i, arr) {
    console.log(i + ": " + item);
});
// 0: "a"
// 1: "b"
// 2: "c"

Therefore, you're creating a quick way to call the forEach function, and you're changing this from the empty array to a list of all <a> tags, and for each <a> in-order, you are calling the function provided.

EDIT

Logical Conclusion / Cleanup

Below, there's a link to an article suggesting that we scrap attempts at functional programming, and stick to manual, inline looping, every time, because this solution is hack-ish and unsightly.

I'd say that while .forEach is less helpful than its counterparts, .map(transformer), .filter(predicate), .reduce(combiner, initialValue), it still serves purposes when all you really want to do is modify the outside world (not the array), n-times, while having access to either arr[i] or i.

So how do we deal with the disparity, as Motto is clearly a talented and knowledgeable guy, and I would like to imagine that I know what I'm doing/where I'm going (now and then... ...other times it's head-first learning)?

The answer is actually quite simple, and something Uncle Bob and Sir Crockford would both facepalm, due to the oversight:

clean it up.

function toArray (arrLike) { // or asArray(), or array(), or *whatever*
  return [].slice.call(arrLike);
}

var checked = toArray(checkboxes).filter(isChecked);
checked.forEach(listValues);

Now, if you're questioning whether you need to do this, yourself, the answer may well be no...
This exact thing is done by... ...every(?) library with higher-order features these days.
If you're using lodash or underscore or even jQuery, they're all going to have a way of taking a set of elements, and performing an action n-times.
If you aren't using such a thing, then by all means, write your own.

lib.array = (arrLike, start, end) => [].slice.call(arrLike, start, end);
lib.extend = function (subject) {
  var others = lib.array(arguments, 1);
  return others.reduce(appendKeys, subject);
};

Update for ES6(ES2015) and Beyond

Not only is a slice( )/array( )/etc helper method going to make life easier for people who want to use lists just like they use arrays (as they should), but for the people who have the luxury of operating in ES6+ browsers of the relatively-near future, or of "transpiling" in Babel today, you have language features built in, which make this type of thing unnecessary.

function countArgs (...allArgs) {
  return allArgs.length;
}

function logArgs (...allArgs) {
  return allArgs.forEach(arg => console.log(arg));
}

function extend (subject, ...others) { /* return ... */ }


var nodeArray = [ ...nodeList1, ...nodeList2 ];

Super-clean, and very useful.
Look up the Rest and Spread operators; try them out at the BabelJS site; if your tech stack is in order, use them in production with Babel and a build step.


There's no good reason not to be able to use the transform from non-array into array... ...just don't make a mess of your code doing nothing but pasting that same ugly line, everywhere.

Morton answered 17/4, 2013 at 6:58 Comment(15)
Now I'm a bit confused cause if I did: console.log(this); I will always get the window object And I thought that .call changes the value of thisFennel
.call does change the value of this, which is why you're using call with the forEach. If forEach looks like forEach (fn) { var i = 0; var len = this.length; for (; i < length; i += 1) { fn( this[i], i, this ); } then by changing this by saying forEach.call( newArrLike ) means that it will use that new object as this.Morton
Yes, but still if I did console.log(this) inside the function of the forEach I should get the nodeList I added via .call because I changed the value of this however what confuses me is that this equals to Window and not the nodeListFennel
@MuhammadSaleh .call doesn't change the value of this inside of what you pass to forEach, .call changes the value of this inside of forEach. If you are confused as to why this doesn't get passed from forEach into the function you wanted called, you should look up the behaviour of this in JavaScript. As an addendum, applicable only here (and map/filter/reduce), there is a second argument to forEach, which sets this inside the function arr.forEach(fn, thisInFn) or [].forEach.call(arrLike, fn, thisInFn); or just use .bind; arr.forEach(fn.bind(thisInFn));Morton
can you please clarify the meaning of this "The rest of the arguments will be passed to the original function."? do you mean [1,2,3]? i tried to console.log arr and got the first argument of call i.e. [a,b,c], not [1,2,3]. so it seems having stuff in the [1,2,3] array or not doesn't actually make a difference yes?Vinegary
@Vinegary that's exactly the point. [] is just there as an array, so that you can .call the .forEach method, and change the this to the set you want to do work on. .call looks like this: function (thisArg, a, b, c) { var method = this; method(a, b, c); } except that there's internal black-magic to set this inside of method to equal what you passed as thisArg.Morton
@Vinegary That's not likely helpful to you, right now; so let's try this: function sum (a, b) { console.log(this.name); return a + b; } var obj = { name: "test" }; sum.call( obj, 1, 2 ); // logs "test", returns 3Morton
so in short... Array.from()?Gingham
@Gingham Array.from(xs).forEach(f); or [...xs].forEach(f);. Now, of course. Not when originally asked/answered.Morton
@Morton yeah, just I'm shocked that some basic things like iterating over node list were even more messy than todayGingham
@Gingham The DOM is notoriously nasty, though has made a lot of strides in the past decade. The expectation would have been that you wrote a for loop, or a while loop, like any good Java/C programmer. The same is true of the arguments list inside of functions; it's not an array, it's a list. So more for loops it is.Morton
@Morton tbh I'm a fan of explicit for loops as you can clearly see what's going on but sometimes you need an array for anything else besides looping over it and then it breaks, that's unfortunateGingham
This answer seems quite incomplete. Please explain why [1, 2, 3].forEach.call iterates three times, but [].forEach.call iterates more than zero times (once?). Also please explain why [1, 2, 3].forEach.call iterates through the indices [0, 1, 2] and not the array values [1, 2, 3].Curate
@LukeHutchison I believe that you are asking questions about the forEach function, and why its second argument is i, where its first argument is a value. Also, I believe you are asking questions about call and why it replaces references to this with whatever you pass in as its first argument. The question about how forEach works in general has little to do with this solution; the question about Function.prototype.call and this is either too deep and particular to explaining low level JS / host behaviour for this answer, or is best taken at face-value as explained.Morton
@LukeHutchison to wit, [].forEach.call(arr, fn) will run for the length of arr, passed into call; not the array that is being used at the start of the forEach (as stated in the first two sentences, and elaborated thereafter). The length of the initial array is wholly inconsequential. When you use the second argument of the forEach function, you are using the index; the same as if you passed the function directly to forEach instead of call. Were you to log the first argument, you would get "a", "b", "c", not 1, 2, 3, due to how call replaces this and how forEach works.Morton
D
65

The querySelectorAll method returns a NodeList, which is similar to an array, but it's not quite an array. Therefore, it doesn't have a forEach method (which array objects inherit via Array.prototype).

Since a NodeList is similar to an array, array methods will actually work on it, so by using [].forEach.call you are invoking the Array.prototype.forEach method in the context of the NodeList, as if you had been able to simply do yourNodeList.forEach(/*...*/).

Note that the empty array literal is just a shortcut to the expanded version, which you will probably see quite often too:

Array.prototype.forEach.call(/*...*/);
Dynamo answered 17/4, 2013 at 6:52 Comment(5)
So, to clarify, there's no advantage in using [].forEach.call(['a','b'], cb) over ['a','b'].forEach(cb) in everyday applications with standard arrays, just when trying to iterate over array-like structures that don't have forEach on their own prototype? Is that correct?Misrepresent
@MattFletcher: Yes that's correct. Both will work but why overcomplicate things? Just call the method directly on the array itself.Dynamo
Cool, thanks. And I don't know why, maybe just for street cred, impressing old ladies in ALDI and such. I'll stick to [].forEach() :)Misrepresent
var section = document.querySelectorAll(".section"); section.forEach((item)=>{ console.log(item.offsetTop) }) works wellMojgan
@Mojgan not in older versions of IE! (that do, however, support forEach on arrays, but not NodeList objects)Gropius
D
22

The other answers have explained this code very well, so I'll just add a suggestion.

This is a good example of code that should be refactored for simplicity and clarity. Instead of using [].forEach.call() or Array.prototype.forEach.call() every time you do this, make a simple function out of it:

function forEach( list, callback ) {
    Array.prototype.forEach.call( list, callback );
}

Now you can call this function instead of the more complicated and obscure code:

forEach( document.querySelectorAll('a'), function( el ) {
   // whatever with the current node
});
Didst answered 17/4, 2013 at 7:0 Comment(0)
B
7
[].forEach.call( document.querySelectorAll('a'), function(el) {
   // whatever with the current node
});

It is basically the same as:

var arr = document.querySelectorAll('a');
arr.forEach(function(el) {
   // whatever with the current node
});
Badminton answered 21/5, 2020 at 19:21 Comment(0)
S
6

It can be better written using

Array.prototype.forEach.call( document.querySelectorAll('a'), function(el) {

});

What is does is document.querySelectorAll('a') returns an object similar to an array, but it does not inherit from the Array type. So we calls the forEach method from the Array.prototype object with the context as the value returned by document.querySelectorAll('a')

Stratify answered 17/4, 2013 at 6:55 Comment(0)
M
5

Want to update on this old question:

The reason to use [].foreach.call() to loop through elements in the modern browsers is mostly over. We can use document.querySelectorAll("a").foreach() directly.

NodeList objects are collections of nodes, usually returned by properties such as Node.childNodes and methods such as document.querySelectorAll().

Although NodeList is not an Array, it is possible to iterate over it with forEach(). It can also be converted to a real Array using Array.from().

However, some older browsers have not implemented NodeList.forEach() nor Array.from(). This can be circumvented by using Array.prototype.forEach() — see this document's Example.

Michael answered 27/11, 2018 at 18:45 Comment(0)
S
4

Lots of good info on this page (see answer+answer+comment), but I recently had the same question as the OP, and it took some digging to get the whole picture. So, here's a short version:

The goal is to use Array methods on an array-like NodeList that doesn't have those methods itself.

An older pattern co-opted Array's methods via Function.call(), and used an array literal ([]) rather than than Array.prototype because it was shorter to type:

[].forEach.call(document.querySelectorAll('a'), a => {})

A newer pattern (post ECMAScript 2015) is to use Array.from():

Array.from(document.querySelectorAll('a')).forEach(a => {})
Sachsen answered 20/5, 2020 at 18:39 Comment(0)
D
3

An empty array has a property forEach in its prototype which is a Function object. (The empty array is just an easy way to obtain a reference to the forEach function that all Array objects have.) Function objects, in turn, have a call property which is also a function. When you invoke a Function's call function, it runs the function with the given arguments. The first argument becomes this in the called function.

You can find documentation for the call function here. Documentation for forEach is here.

Donothing answered 17/4, 2013 at 6:51 Comment(0)
C
1

Just add one line:

NodeList.prototype.forEach = HTMLCollection.prototype.forEach = Array.prototype.forEach;

And voila!

document.querySelectorAll('a').forEach(function(el) {
  // whatever with the current node
});

Enjoy :—)

Warning: NodeList is a global class. Don't use this recomendation if you writing public library. However it's very convenient way for increasing self-efficacy when you work on website or node.js app.

Cutoff answered 18/9, 2015 at 5:42 Comment(0)
M
1

Just a quick and dirty solution I always end up using. I wouldn't touch prototypes, just as good practice. Of course, there are a lot of ways to make this better, but you get the idea.

const forEach = (array, callback) => {
  if (!array || !array.length || !callback) return
  for (var i = 0; i < array.length; i++) {
    callback(array[i], i);
  }
}

forEach(document.querySelectorAll('.a-class'), (item, index) => {
  console.log(`Item: ${item}, index: ${index}`);
});
Mainland answered 2/7, 2018 at 5:59 Comment(0)
T
0

[] always returns a new array, it is equivalent to new Array() but is guaranteed to return an array because Array could be overwritten by the user whereas [] can not. So this is a safe way to get the prototype of Array, then as described, call is used to execute the function on the arraylike nodelist (this).

Calls a function with a given this value and arguments provided individually. mdn

Treen answered 17/4, 2013 at 7:0 Comment(2)
While [] will in fact always return an array, while window.Array can be overwritten with anything (in all old browsers), so too can window.Array.prototype.forEach be overwritten with anything. There is no guarantee of safety in either case, in browsers which don't support getters/setters or object sealing/freezing (freezing causing its own extensibility issues).Morton
Feel free to edit the answer to add the extra information regarding the possibility of overwriting the prototype or it's methods: forEach.Treen
L
0

Norguard explained WHAT [].forEach.call() does and James Allardice WHY we do it: because querySelectorAll returns a NodeList that doesn't have a forEach method...

Unless you have modern browser like Chrome 51+, Firefox 50+, Opera 38, Safari 10.

If not you can add a Polyfill:

if (window.NodeList && !NodeList.prototype.forEach) {
    NodeList.prototype.forEach = function (callback, thisArg) {
        thisArg = thisArg || window;
        for (var i = 0; i < this.length; i++) {
            callback.call(thisArg, this[i], i, this);
        }
    };
}
Litterbug answered 27/9, 2017 at 10:51 Comment(0)
T
0

let's say you have : const myList= document.querySelectorAll("p"); This will return an list/array of all

in your HTML. Now Array.prototype.forEach.call(myList, myCallback) is equivalent to [].forEach.call(myList, myCallback) where 'myCallback' is a callback function. You are basically running the callback function on each element of myList. Hope this helped you!

Triceps answered 26/7, 2022 at 3:7 Comment(0)
A
-1

I don't know if there is any restriction, but it works. I turned the nodeList into an iterator object using the spread operator and mapped it:

let _btns = document.querySelectorAll('.btn');

[..._btns].map(function(elem, i) {
  elem.addEventListener('click', function (e) {
    console.log(elem.textContent);
  })
})
.btn {
  padding: 5px; 
  color:#fff; 
  background-color: darkred; 
  text-align:center;
  color: white;
}
<button class="btn">button 1</button>
<button class="btn">button 2</button>
A1 answered 9/2, 2023 at 14:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.