Array internals, the methods you actually use, and writing your own
A JavaScript array is not what a C programmer thinks it is. It is an object. The numeric keys and the length property are the only things that make it feel array-like. Once you accept that, the interview answers get easier.
This lesson is for the moment an interviewer slides a marker across the table and says, "write your own .map."
Arrays are objects, sparse vs dense
Run this in your console and read it slowly.
const cart = ["paneer", "atta", "ghee"];
cart[10] = "dahi";
console.log(cart.length); // 11
console.log(cart[5]); // undefined
console.log(Object.keys(cart)); // ["0","1","2","10"]The engine stored four real entries but length jumped to 11. That is a sparse array. The slots in between are not zeros, they are holes. V8 keeps two internal representations: packed (dense) and holey (sparse). The moment you create a hole, the array drops to a slower path and many optimisations turn off.
Mental model:
Senior rule: never use new Array(1000) to preallocate in JS. You just made a holey array. Use Array.from({ length: 1000 }, () => 0) instead.
The four you actually use
Ninety percent of the array code in a Swiggy order service is one of these.
map(fn)returns a new array of the same length, each item transformed.filter(fn)returns a new array with items wherefnis truthy.reduce(fn, init)folds the array into a single value (sum, group, max).forEach(fn)runsfnfor each item, returnsundefined. Use when you need a side effect, not a transform.
const orders = [{ amt: 250 }, { amt: 180 }, { amt: 999 }];
const big = orders.filter(o => o.amt > 200); // [{250},{999}]
const amts = orders.map(o => o.amt); // [250,180,999]
const total = orders.reduce((sum, o) => sum + o.amt, 0); // 1429
orders.forEach(o => console.log(`Order ₹${o.amt}`));Interview trap:
forEachignores the return value of its callback and cannot beawait-ed cleanly. If you need async iteration, use a plainfor...ofloop withawaitinside.
The one-liners
find(fn)returns the first matching item, orundefined.some(fn)returnstrueif at least one item matches.every(fn)returnstrueonly if all items match.includes(x)returnstrueifxis in the array (usesSameValueZero, soNaNworks).indexOf(x)returns the index or-1(uses===, soNaNdoes not work).
Predict output: [NaN].includes(NaN) is true, [NaN].indexOf(NaN) is -1. That is a real interview question.
Mutating vs non-mutating
This is the bug that ships to production.
const a = [3, 1, 2];
const b = a.sort(); // sorts a in place, returns same ref
console.log(a === b); // true -> a is now [1,2,3]
const c = [3, 1, 2];
const d = [...c].sort(); // copy first, then sort
console.log(c === d); // false -> c is untouched- Mutating:
push,pop,shift,unshift,splice,sort,reverse,fill. - Non-mutating:
map,filter,slice,concat, spread[...a], and the newtoSorted,toReversed,toSpliced,with(ES2023).
In React, mutating state arrays will not trigger a rerender because the reference did not change. Always copy first.
Polyfill: write your own .map
Array.prototype.myMap = function (cb) {
if (typeof cb !== "function") throw new TypeError("cb must be a function");
const out = new Array(this.length);
for (let i = 0; i < this.length; i++) {
if (i in this) out[i] = cb(this[i], i, this);
}
return out;
};
[1, 2, 3].myMap(x => x * 10); // [10, 20, 30]Step-by-step execution:
- Validate the callback. Senior signal.
- Preallocate the output to the same length.
- Loop with a classic
for, notfor...of(faster, lets us check holes). i in thisskips holes, matching native.mapbehaviour.- Pass
(value, index, array)to the callback. The third argument is what most candidates forget.
Polyfill: write your own .reduce
Array.prototype.myReduce = function (cb, init) {
let acc = init;
let i = 0;
if (acc === undefined) {
if (this.length === 0) throw new TypeError("Reduce of empty array with no initial value");
acc = this[0];
i = 1;
}
for (; i < this.length; i++) {
if (i in this) acc = cb(acc, this[i], i, this);
}
return acc;
};Trap: when no
initis passed, the first element becomes the accumulator and iteration starts at index 1. Most candidates miss this and the empty-arrayTypeError.
Polyfill: write your own .filter
Array.prototype.myFilter = function (cb) {
const out = [];
for (let i = 0; i < this.length; i++) {
if (i in this && cb(this[i], i, this)) out.push(this[i]);
}
return out;
};Performance, the numbers worth memorising
push,pop: O(1) amortised. End of the array, no shifting.shift,unshift: O(n). Every other element moves by one.spliceat the middle: O(n).map,filter,reduce,forEach: O(n), single pass.indexOf,find,includes: O(n), worst case scans everything.sort: O(n log n), TimSort in V8.
If you find yourself doing arr.shift() inside a loop on 100k items, you have written O(n²). Use a queue index or a real queue.
Array internals
- Packed array
- All indices 0..length-1 are filled. V8 uses contiguous storage and the fast path.
- Holey array
- Has gaps (holes). V8 falls back to a dictionary-like representation, slower.
- Mutating method
- Changes the original array. push, sort, splice. Returns the same reference.
- Non-mutating method
- Returns a new array. map, filter, slice, spread. Safe for React state.
- SameValueZero
- Comparison used by includes and Set. Like === but NaN equals NaN.
Senior rule: if you cannot write reduce from scratch in 5 minutes, you do not understand reduce yet. Practise it until your hand types it without thought.
Quick quiz, prove you got it
- 1.Which methods MUTATE the original array?
- 2.Big O of `arr.shift()` on a length-n array?
- 3.What does `[1,2,3].reduce((acc, x) => acc + x)` return (no initial value)?
Interview insights
When asked to "implement map from scratch"
- Add to Array.prototype OR write a standalone function
- Pass element, index, and the array itself to the callback
- Return a NEW array (do not mutate this)
- Handle empty arrays and sparse arrays
Tricky questions they will ask
Q.What does `[].reduce((a,b) => a + b)` throw?›
A.TypeError: Reduce of empty array with no initial value. Always pass an initial value if the array might be empty.
Q.Why is sort() weird with numbers?›
A.By default, sort converts elements to strings and compares lexicographically. [10, 2, 1].sort() gives [1, 10, 2]. Always pass a compare function: [10,2,1].sort((a,b) => a - b).
Free tools you can use while you learn
Common questions
Q.How can I tell mutating from non-mutating array methods?›
Q.Why are shift and unshift slower than push and pop?›
Watching quietly. Tap me if you want a tip.
Try this (0 of 2 done)
- 1
Implement myFilter on Array.prototype. Use it to keep only even numbers from [1..6].
show answer
Array.prototype.myFilter = function(cb) { const out = []; for (let i = 0; i < this.length; i++) if (cb(this[i], i, this)) out.push(this[i]); return out; }; console.log([1,2,3,4,5,6].myFilter(n => n % 2 === 0)); - 2
Implement myReduce. Use it to sum [10, 20, 30].
show answer
Array.prototype.myReduce = function(cb, init) { let acc = init; for (let i = 0; i < this.length; i++) acc = cb(acc, this[i], i, this); return acc; }; console.log([10,20,30].myReduce((a,b) => a + b, 0));