Why does calling .map() on the result of document.querySelectorAll() throw, and how do you get array methods over the elements it returns?
answer
- it is not an Array
- array-like: length plus integer indices
- no Array.prototype in the chain
- forEach yes, map no
- Array.from or spread converts it
basics
~20 sdocument.querySelectorAll returns a NodeList, not an Array. It is array-like and iterable and does have forEach, but it does not inherit from Array.prototype, so map, filter and reduce are missing. Convert with Array.from(nodes) or [...nodes].
solid answer
~40 s`document.querySelectorAll()` returns a `NodeList`, which is a DOM collection rather than an `Array`. It is *array-like* — it has a `length` and integer indices — and it is iterable, and `NodeList` does define `forEach`, `entries`, `keys` and `values`. What it does not do is inherit from `Array.prototype`, so `map`, `filter`, `reduce`, `slice` and `find` are simply absent and calling one throws `TypeError: ... is not a function`. `HTMLCollection`, which `getElementsByClassName` and `element.children` return, is thinner still: it has no `forEach` at all. The fix is to convert once and then work with a real array — `Array.from(nodes)` or `[...nodes]`. `Array.from` also takes a mapping function as its second argument, so you can go straight from the collection to the values you want.
code
javascript · 9 linesconst items = document.querySelectorAll('li');
items.forEach(el => console.log(el.textContent)); // works
// items.map(el => el.textContent); // TypeError: items.map is not a function
const texts = Array.from(items, el => el.textContent);
const visible = [...items].filter(el => !el.hidden);
console.log(Array.isArray(items), Array.isArray(texts)); // false truego deeper
Know that querySelectorAll gives back a NodeList rather than an array, recognise the TypeError from calling map on it, and reach for Array.from or spread.
Explain why: the prototype chain has no Array.prototype, and the iterable declaration is what supplies forEach and Symbol.iterator but nothing that returns a new array. Contrast NodeList with HTMLCollection.
Show judgment about where the conversion belongs — convert once at the boundary rather than scattering Array.prototype.method.call, and know when forEach in place is enough to avoid the allocation.
Own the convention across a codebase: a single helper or lint rule that normalises DOM collections into arrays at the edges, so downstream code never has to know which collection type it was handed.
## What you actually get back DOM collections are defined by the DOM specification, not by ECMAScript. They are objects whose prototype chain is `NodeList.prototype` → `Object.prototype`; `Array.prototype` appears nowhere in it. That single fact explains every symptom: ```js const items = document.querySelectorAll('li'); typeof items; // "object" Array.isArray(items); // false items.length; // a number items[0]; // the first element items.map; // undefined -> calling it throws a TypeError ``` ## "Array-like" is a shape, not a type An object is *array-like* when it has a non-negative integer `length` and properties at integer keys. That is a duck-typing convention, not an interface you implement. Array-like objects support `for` loops, index access, and `length`, and that is all they are guaranteed to support. The built-in array methods work on array-like receivers by design — they read `length` and index into `this` — which is why the `.call` trick works even though the collection is not an array: ```js Array.prototype.map.call(items, el => el.textContent); ``` ## Why NodeList has forEach but not map `NodeList` is declared in the DOM spec as `iterable<Node>`. That declaration is what gives it, automatically, exactly five things: `forEach`, `entries`, `keys`, `values`, and the `Symbol.iterator` method that makes `for...of` and spread work. It does not pull in the rest of the array surface, because those methods return *new arrays* and a DOM collection is not in the business of constructing arrays. So the asymmetry that looks arbitrary — `forEach` yes, `map` no — is a direct consequence of one line of interface definition. `HTMLCollection`, returned by `getElementsByClassName`, `getElementsByTagName`, `element.children`, `document.forms` and `document.images`, is not declared iterable. It gets `length`, `item(index)` and `namedItem(name)` and nothing else, so it has no `forEach` either. Because it exposes indexed properties it still gets a default iterator, which is why spread and `for...of` work on it even though method-style iteration does not: ```js const kids = document.body.children; // HTMLCollection typeof kids.forEach; // "undefined" [...kids].length === kids.length; // true — spreading works ``` ## Converting, and which conversion to use All three of these produce a genuine `Array` whose elements are *the same element objects* — nothing is cloned, and mutating an element through the array mutates the live document node: ```js const a = Array.from(items); const b = [...items]; const c = Array.prototype.slice.call(items); // the pre-ES2015 idiom ``` `Array.from` has one capability the others lack: a second argument that maps as it converts, avoiding an intermediate array. ```js const texts = Array.from(items, el => el.textContent); ``` Spread reads a little cleaner in expressions; `Array.from` is the more explicit choice and is the only one that also handles plain array-like objects with no iterator at all (an object with just `length` and index keys). That last difference is the one people trip over: `[...obj]` throws for a non-iterable array-like, while `Array.from(obj)` works. What does *not* work is `Object.values` on the idea that it will "just give you the elements" (it returns index-keyed values in a way that is easy to get wrong across collection types), and `JSON.stringify` round-tripping (element objects do not survive serialization). ## Practical shape of the code The habit worth forming is: convert at the boundary, then stay in array land. ```js const visible = [...document.querySelectorAll('.row')] .filter(el => !el.hidden) .map(el => el.dataset.id); ``` If all you need is a single pass with no return value, `items.forEach(...)` is fine and saves the allocation — but the moment you want `filter`, `map`, `reduce`, `some`, `sort` or `slice`, convert first rather than reaching for `Array.prototype.<method>.call`, which is harder to read and no faster in any way that matters. ## The error message you will actually see It is worth recognising on sight: `TypeError: items.map is not a function`. In the DOM context that message almost always means "this is a collection, not an array", and the fix is a conversion, not a different selector.
- What exactly makes an object array-like in JavaScript?A non-negative integer `length` property plus properties at integer keys. That is the whole contract — it says nothing about the prototype. `arguments`, DOM collections and even a hand-made `{0:'a', 1:'b', length:2}` all qualify, which is why `Array.prototype.slice.call(obj)` and `Array.from(obj)` work on all of them while `obj.slice()` works on none of them.
- When would you prefer Array.from over the spread syntax for a conversion?Two cases. When you want to map during the conversion, `Array.from(nodes, el => el.id)` skips the intermediate array. And when the source is array-like but has no `Symbol.iterator` — a plain object with `length` and index keys — `Array.from` still works while `[...obj]` throws a TypeError. For an ordinary NodeList either form is fine.
- Does converting a collection to an array copy the elements?No. The array holds references to the very same element objects, so writing `arr[0].textContent = 'x'` changes the document. What the conversion copies is the *membership list* — the array is a fixed snapshot of which nodes were in the collection at that moment, and later document changes do not add or remove entries from it.
saying these in an interview costs you the question
- querySelectorAll returns an array
- It has forEach, so it must have map too
- Array.from clones the DOM nodes into new elements
- NodeList and HTMLCollection expose the same methods
- JSON.stringify is a fine way to turn it into an array