1
var array = [];
document.querySelectorAll("a").forEach(array.push); //Uncaught TypeError: Cannot convert undefined or null to object

Why does this fail? what does this error mean? It seems perfectly reasonable to pass an array method as a function, what am I not understanding/seeing?

Sam
  • 39
  • 6
  • 1
    NodeLists are not arrays, you need to coerce them into arrays https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/ – Dominic Sep 11 '18 at 20:11
  • I should not have used a nodeList in my example, it's unrelated to the question I'm asking and seems to have been a real red herring. – Sam Sep 11 '18 at 20:48

6 Answers6

4

array.push loses context (its this), so you need to pass a function with context captured.

But, even if what you wanted worked - you still would not get the result you want, since NodeList:forEach passes 3 arguments to the callback function, so you would fill your array with elements, indexes and the list of nodes.

So a solution would be to do

var array = [];
document.querySelectorAll("a").forEach(e => array.push(e));
zerkms
  • 230,357
  • 57
  • 408
  • 498
  • can you elaborate on context? I thought any function could be passed to a foreach and execute with relative arguments (var, index, idk) , is that not how foreach works? – Sam Sep 11 '18 at 20:42
  • @Sam short answer - `this` is not preserved when you pass it like that, so a reference to `array` is lost. Long answer: https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work – zerkms Sep 11 '18 at 21:18
  • I don't think context is the relevant bit in this case. document.querySelectorAll("a").forEach(function(e) { array.push(e)}) would work just as well wouldn't it? – Steve Archer Sep 11 '18 at 23:03
  • @SteveArcher the lost context is the very reason of the exception from the question: `Uncaught TypeError: Cannot convert undefined or null to object`: it's caused by the `Array.prototype.push` being unable to push a value to the `undefined`. The function expression syntax indeed would work as well. – zerkms Sep 11 '18 at 23:05
  • but the function expression syntax doesn't pass scope down like an arrow function does. I think the reason it's erroring is because he's not giving push anything to push. The parameter is the thing that's undefined, not array. – Steve Archer Sep 11 '18 at 23:07
  • 1
    Here is how you can reproduce the problem easier: `Array.prototype.push.call(undefined, 42)` Expression function does not need to carry any context since you're not using `this`, but the `array` context is kept given you're calling it as a `array.push()`. "The parameter is the thing that's undefined, not array" --- parameters (all 3 of them) are passed on the caller side: inside the `NodeList#forEach`. Take a `NodeList#forEach` polyfill and step through it with a debugger to better see what's happening. – zerkms Sep 11 '18 at 23:08
2

You can rebind the this context

var arr = []; document.querySelectorAll("a").forEach(arr.push.bind(arr));
console.log(arr);
<a href="#foo1">one</a>
<a href="#foo2">two</a>
<a href="#foo3">three</a>

BUT the big issue here is the fact that push() when given multiple arguments, will add all of those arguments to the array.

So push(1,2,3) will add 1, 2, and 3 to the array. So the above code would have 3 links, but it will add 9 entries into the array because forEach has 3 arguments element, index, array. So you will need to use a function to do it. Or just use Array.from() to create the array.

var arr = []; 
document.querySelectorAll('a').forEach(el => arr.push(el))
console.log(arr);

var arr2 = Array.from(document.querySelectorAll('a'))
console.log(arr2);
<a href="#foo1">one</a>
<a href="#foo2">two</a>
<a href="#foo3">three</a>
epascarello
  • 185,306
  • 18
  • 175
  • 214
  • brilliant, do you have an explanation (or link) as to why push loses it's context? thanks for your answer! Array.from is new to me and looks *incredibly* useful. – Sam Sep 11 '18 at 20:47
1

The problem at first instance is not passing array.push, the problem is you are iterating over a NodeLists and this structure is not an array, you can use Array.from in next way:

const array =Array.from(document.querySelectorAll("a"));

Jose Mato
  • 2,505
  • 1
  • 13
  • 17
  • 1
    "and this structure is not an array" --- so what? It still has a `forEach` – zerkms Sep 11 '18 at 20:14
  • This does not work. You cannot do a `forEach` on `NodeList` – Raul Rene Sep 11 '18 at 20:15
  • I think it depends on the browser? – Steve Archer Sep 11 '18 at 20:15
  • @zerkms, not all browser has the implementation done, and your comment "so what?" I guesss is well explained, is not array. – Jose Mato Sep 11 '18 at 20:18
  • 1
    @JoseMato what browsers support `Array.from` and do not support `NodeList:forEach`? The problem is not in the lack of support of `forEach`. If `forEach` was missing it would report that undefined is not a function. – zerkms Sep 11 '18 at 20:18
1

When you extract a function from an object, it loses its context, so when you call it and it accesses this, its original value has been lost.

To fix the issue you need to use Function.prototype.bind() to keep the this reference pointing to the right object.

You can see the problem and how bind works in this example:

const obj = {
  prop: 'there',

  print(prefix) {
    console.log(`${ prefix }: Hello ${ this.prop }.`);
  }
};

obj.print('obj.print');

// Context lost:

const extractedPrint = obj.print;

extractedPrint('extractedPrint');

// Context preserved to the original object:

const bindedPrint = obj.print.bind(obj);

bindedPrint('bindedPrint');

// Context replaced:

const alsoBindedPrint = obj.print.bind({ prop: 'bro' });

alsoBindedPrint('alsoBindedPrint');

Wondering where is this pointing when it's "lost"? It points to window:

const obj = {
  prop: 'there',

  print(prefix) {
    console.log(`${ prefix }: Hello ${ this.prop }.`);
  }
};

const extractedPrint = obj.print;

window.prop = 'window';

extractedPrint('extractedPrint');

In your case, you need to make sure that when push is called by forEach its context is preserved, that is, its this value should still be referencing the original array:

links.forEach(array.push.bind(array));

Anyway, that won't work as expected because NodeList.prototype.forEach() calls its callback with 3 arguments: currentValue, currentIndexand listObj and Array.prototype.push() accepts multiple arguments at once, so you could do:

const array = [];
const links = document.querySelectorAll('a');

links.forEach(array.push.bind(array));

console.log(array.length);
<a>1</a>
<a>2</a>
<a>3</a>

But for each Node or your NodeList, you would be calling push with 3 arguments instead of 1, ending up getting some unwanted elements on the list.

To convert a NodeList to an Array you could use Array.from() instead:

console.log(Array.from(document.querySelectorAll('a')).length);
<a>1</a>
<a>2</a>
<a>3</a>

Although there are other ways to do it, like pushing all the elements one by one defining your own callback:

const links = document.querySelectorAll('a');
const arr = [];

// Note we only define a single argument, so we ignore the other 2:
links.forEach((link) => {
  arr.push(link);
});

console.log(arr);
<a>1</a>
<a>2</a>
<a>3</a>

Or the the same thing using a loop:

const links = document.querySelectorAll('a');
const arr = [];

for (const link of links) {
  arr.push(link);
}

// Also valid:

// for (let i = 0; i < links.length; ++i) {
//   arr.push(links[i]);
// }

console.log(arr);

// Note this won't work, as NodeList has some other iterable
// properties apart from the indexes:

const arr2 = [];

for(const i in links) {
  arr2.push(links[i])
}

console.log(arr2);
<a>1</a>
<a>2</a>
<a>3</a>
Danziger
  • 13,604
  • 4
  • 30
  • 57
0

I'm not 100% sure what you're trying to do, but I'm going to guess that you'd like to push the elements returned by querySelectorAll into 'array'. In which case, you can't just pass the function push to forEach, you must call push on array with each element as the argument, like so:

document.querySelectorAll("a").forEach(item => array.push(item));
Steve Archer
  • 642
  • 4
  • 10
  • yes, but i'm trying to understand why i need to wrap it in a function (when push is already a function) – Sam Sep 11 '18 at 20:44
  • just passing it a function isn't good enough, you need to pass it a function that CALLS push with the thing it needs to push (the parameter passed in the function call). It's the difference between just giving someone a truck, and giving someone a truck and a package and telling them to deliver it. – Steve Archer Sep 11 '18 at 23:00
0

What are you trying to push in your example? Maybe you are missing some parameters?

Try it out:

document.querySelectorAll("a").forEach(function(item) {
    array.push(item);
});
guicervo
  • 46
  • 5