ToolPopToolPop
JavaScript · Lesson 16 of 18

Array internals, the methods you actually use, and writing your own

8 min readUpdated 24 Jun 2026

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.

js
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:

Diagram
rendering diagram...

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 where fn is truthy.
  • reduce(fn, init) folds the array into a single value (sum, group, max).
  • forEach(fn) runs fn for each item, returns undefined. Use when you need a side effect, not a transform.
js
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: forEach ignores the return value of its callback and cannot be await-ed cleanly. If you need async iteration, use a plain for...of loop with await inside.

The one-liners

  • find(fn) returns the first matching item, or undefined.
  • some(fn) returns true if at least one item matches.
  • every(fn) returns true only if all items match.
  • includes(x) returns true if x is in the array (uses SameValueZero, so NaN works).
  • indexOf(x) returns the index or -1 (uses ===, so NaN does 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.

js
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 new toSorted, 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

js
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:

  1. Validate the callback. Senior signal.
  2. Preallocate the output to the same length.
  3. Loop with a classic for, not for...of (faster, lets us check holes).
  4. i in this skips holes, matching native .map behaviour.
  5. Pass (value, index, array) to the callback. The third argument is what most candidates forget.

Polyfill: write your own .reduce

js
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 init is passed, the first element becomes the accumulator and iteration starts at index 1. Most candidates miss this and the empty-array TypeError.

Polyfill: write your own .filter

js
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.
  • splice at 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

0/3 answered
  1. 1.Which methods MUTATE the original array?
  2. 2.Big O of `arr.shift()` on a length-n array?
  3. 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?
common wrong answer:Returns 0

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?
A.Mutating: push, pop, shift, unshift, splice, sort, reverse. Non-mutating (return a new array): map, filter, slice, concat, flat, toSorted, toReversed. When you do not want to change the original, prefer non-mutating.
Q.Why are shift and unshift slower than push and pop?
A.shift and unshift change index 0, which means every other element must be re-indexed. That is O(n). push and pop touch only the end, which is O(1).
Chai0/2 done

Watching quietly. Tap me if you want a tip.

JS Playground
console
// hit run to see output

Try this (0 of 2 done)

  1. 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. 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));