问题
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?
回答1:
[]
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:
- the element in the array
- the index of the element (third element would pass
2
) - 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.
回答2:
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(/*...*/);
回答3:
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
});
回答4:
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')
回答5:
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.
回答6:
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.
回答7:
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}`);
});
回答8:
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.
回答9:
[]
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
回答10:
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);
}
};
}
来源:https://stackoverflow.com/questions/16053357/what-does-foreach-call-do-in-javascript