I frequently want to iterate through a NodeList with forEach or map. My simplified code works like this:
var nodeListMap = Array.prototype.map;
var els = document.querySelectorAll('.classname');
nodeListMap.call(els, function(el){...});
This works fine. However, I'd prefer not have to map.call
, but if I do this...
var nodeListMap = Array.prototype.map.call;
var els = document.querySelectorAll('.classname');
nodeListMap(els, function(el){...});
Then it returns
TypeError: object is not a function
How can I modify the code so I simply do nodeListMap(array, fn)
?
I frequently want to iterate through a NodeList with forEach or map. My simplified code works like this:
var nodeListMap = Array.prototype.map;
var els = document.querySelectorAll('.classname');
nodeListMap.call(els, function(el){...});
This works fine. However, I'd prefer not have to map.call
, but if I do this...
var nodeListMap = Array.prototype.map.call;
var els = document.querySelectorAll('.classname');
nodeListMap(els, function(el){...});
Then it returns
TypeError: object is not a function
How can I modify the code so I simply do nodeListMap(array, fn)
?
5 Answers
Reset to default 9I've encountered the same question and my super simple solution with ES6 is:
const els = [...document.querySelectorAll('.classname')]
Such way nodeList bees a regular Array and you can use map
, reduce
and so on.
Array.prototype.map.call
just gets you the call
function (Function.prototype.call
), without the context. You will need to bind it to the map
function:
var nodeListMap = Function.prototype.call.bind(Array.prototype.map);
If you don't want to use bind
, you could also write
function nodeListMap(_list /* … */) {
return Function.prototype.call.apply(Array.prototype.map, arguments);
}
It's probably simplest just to write your own function that "does the right thing"
function map() {
var args = [].slice.call(arguments, 0);
var ctx = args.shift();
return [].map.apply(ctx, args);
}
which will then work for any pseudo-array object.
EDIT this code is updated to ensure that all arguments are passed to .map
, even if ECMA add more in the future.
Another option would be to add this functionality to the prototype NodeList:
NodeList.prototype.map = function(step){
return Array.prototype.map.call(this, step);
};
NodeList.prototype.forEach = function(step){
return Array.prototype.forEach.call(this, step);
};
With this, you could just call:
els.map(function(el){...});
It should be noted that some people would likely frown upon modifying the prototype of NodeList in this way, but I don't really see a problem with it personally.
Or, if you need to set the this
object:
NodeList.prototype.map = function(step){
return Array.prototype.map.call(this, step, Array.prototype.slice.call(arguments, 1));
};
NodeList.prototype.forEach = function(step){
return Array.prototype.forEach.call(this, step, Array.prototype.slice.call(arguments, 1));
};
Note: the above has a side effect that when you don't pass a second parameter, this
bees an empty array instead of window
.
or
NodeList.prototype.map = Array.prototype.map;
NodeList.prototype.forEach = Array.prototype.forEach;
After six years the map
and forEach
methods are available on NodeLists on most browsers (the odd duck being IE usual). Check forEach
nodeList support on caniuse.
If older browser support is needed, I have settled on the following for brevity and clarity (ES6+):
const els = Array.from(document.querySelectorAll('.selector'));
Then, you can simply iterate as...
els.forEach(el => doSomething)