Episode 1 — Fundamentals / 1.21 — Arrays

Interview Questions: JavaScript Arrays

Model answers for array creation, indexing and access, mutation vs immutability, iteration patterns, performance, and multidimensional arrays.

How to use this material (instructions)

  1. Read lessons in order -- README.md, then 1.21.a -> 1.21.g.
  2. Practice out loud -- definition -> example -> pitfall.
  3. Pair with exercises -- 1.21-Exercise-Questions.md.
  4. Quick review -- 1.21-Quick-Revision.md.

Beginner (Q1--Q6)

Q1. What is an array in JavaScript?

Why interviewers ask: Confirms fundamental understanding of the most-used data structure.

Model answer:

An array is an ordered collection of values, stored under a single variable name and accessed by zero-based numeric indices. Arrays in JavaScript are objects (typeof [] === "object") with special behavior: automatic length tracking, numeric indexing, and a rich set of built-in methods (push, pop, map, filter, etc.). They can hold any type of value -- numbers, strings, objects, or even other arrays -- and their length is dynamic (no fixed size).

const fruits = ["apple", "banana", "cherry"];
console.log(fruits[0]);       // "apple"
console.log(fruits.length);   // 3
console.log(Array.isArray(fruits)); // true

Q2. What is the difference between new Array(3) and [3]?

Why interviewers ask: Tests awareness of a classic JS gotcha.

Model answer:

new Array(3) creates a sparse array with 3 empty slots and length of 3, but no actual elements. [3] creates an array with one element, the number 3, and length of 1.

const a = new Array(3);  // [ <3 empty items> ] -- length 3, no elements
const b = [3];           // [3] -- length 1, one element

console.log(a.length);   // 3
console.log(b.length);   // 1
console.log(a[0]);       // undefined (hole)
console.log(b[0]);       // 3

Array.of(3) was introduced in ES6 to fix this ambiguity -- it always treats the argument as an element: Array.of(3) returns [3].


Q3. How do arrays behave as reference types?

Why interviewers ask: Critical for understanding mutation bugs, function arguments, and equality.

Model answer:

Arrays are objects, so variables hold a reference (pointer), not the data itself. Assigning an array to another variable copies the reference, not the array. Both variables point to the same array in memory.

const a = [1, 2, 3];
const b = a;        // b points to the SAME array
b.push(4);
console.log(a);     // [1, 2, 3, 4] -- a is affected

// Consequence for equality:
[1, 2] === [1, 2];  // false -- different references
const x = [1, 2];
const y = x;
x === y;             // true -- same reference

To copy: Use [...arr], Array.from(arr), or arr.slice() for a shallow copy. For deep copies (nested arrays/objects), use structuredClone(arr).


Q4. Can you modify the contents of a const array?

Why interviewers ask: Tests understanding of const semantics vs immutability.

Model answer:

Yes. const prevents reassignment of the variable, not modification of the object it points to. You can push, pop, change indices, and call any mutating method. To make the array truly immutable (shallowly), use Object.freeze().

const arr = [1, 2, 3];
arr.push(4);         // OK -- modifying contents
arr[0] = 99;         // OK
// arr = [5, 6];     // TypeError -- cannot reassign

const frozen = Object.freeze([1, 2, 3]);
frozen.push(4);      // TypeError in strict mode

Q5. What does arr.at(-1) do and when would you use it?

Why interviewers ask: Tests awareness of modern ES2022 features.

Model answer:

at() works like bracket notation but supports negative indices that count from the end. arr.at(-1) returns the last element, arr.at(-2) the second-to-last, etc.

const arr = ["a", "b", "c", "d"];
arr.at(-1);   // "d"
arr.at(-2);   // "c"
arr[arr.length - 1]; // "d" -- the old way

Use it when accessing from the end, especially in chained expressions where computing length - 1 is verbose.


Q6. What is the difference between indexOf, includes, find, and findIndex?

Why interviewers ask: Common search operations -- interviewers want to see you pick the right tool.

Model answer:

MethodReturnsBest for
indexOf(val)Index (or -1)Finding the position of a primitive value
includes(val)BooleanChecking existence of a primitive (handles NaN correctly)
find(fn)Element (or undefined)Finding the first element matching a condition
findIndex(fn)Index (or -1)Finding the index of the first match by condition
const arr = [1, 2, NaN, 4];
arr.indexOf(NaN);        // -1 (broken -- uses ===)
arr.includes(NaN);       // true (correct)

const users = [{id:1},{id:2},{id:3}];
users.find(u => u.id === 2);      // {id: 2}
users.findIndex(u => u.id === 2); // 1

Intermediate (Q7--Q12)

Q7. Explain the performance difference between push/pop and unshift/shift.

Why interviewers ask: Tests algorithmic thinking and understanding of underlying data structures.

Model answer:

push/pop operate on the end of the array and are O(1) amortized -- no other elements need to move. unshift/shift operate on the beginning and are O(n) because every existing element must be re-indexed (shifted one position).

push(x):    [A, B, C] + x = [A, B, C, x]    -- nothing moves
unshift(x): x + [A, B, C] = [x, A, B, C]    -- A, B, C all shift right

For large arrays, frequent unshift/shift can be a performance bottleneck. If you need efficient front insertion, consider a deque or linked list structure.


Q8. What is the difference between mutating and non-mutating array methods?

Why interviewers ask: Mutation bugs are a top source of defects in JavaScript.

Model answer:

Mutating methods change the original array in place: push, pop, shift, unshift, reverse, sort, splice, fill, copyWithin.

Non-mutating methods return a new array and leave the original unchanged: concat, slice, map, filter, reduce, flat, toReversed (ES2023), toSorted (ES2023).

// Mutating
const a = [3, 1, 2];
a.sort();          // a is now [1, 2, 3]

// Non-mutating
const b = [3, 1, 2];
const c = b.toSorted();  // b is still [3, 1, 2]; c is [1, 2, 3]

Best practice: In functional programming and frameworks like React, prefer non-mutating operations to avoid unintended side effects and ensure proper state change detection.


Q9. How does length behave on sparse arrays?

Why interviewers ask: Tests deeper understanding beyond surface-level usage.

Model answer:

length is always one more than the highest numeric index, not a count of actual elements. Sparse arrays have holes -- indices with no assigned value.

const sparse = [];
sparse[0] = "a";
sparse[100] = "z";
console.log(sparse.length);  // 101 (not 2)

// Iteration methods treat holes differently:
// forEach, map, filter SKIP holes
// for loop reads holes as undefined
// for...of reads holes as undefined

This is why sparse arrays should be avoided -- they cause inconsistent behavior across different iteration methods.


Q10. Explain array destructuring with the rest pattern.

Why interviewers ask: Modern syntax used everywhere in React, Node.js, and functional JS.

Model answer:

Destructuring extracts values from arrays into named variables. The rest pattern ...rest collects remaining elements into a new array.

const [first, second, ...rest] = [10, 20, 30, 40, 50];
console.log(first);  // 10
console.log(second); // 20
console.log(rest);   // [30, 40, 50]

// Skipping elements
const [a, , b] = [1, 2, 3];  // a=1, b=3

// Default values
const [x = 0, y = 0] = [42];  // x=42, y=0

// Swapping
let p = 1, q = 2;
[p, q] = [q, p];  // p=2, q=1

The rest element must be the last in the pattern. It creates a shallow copy of the remaining elements.


Q11. Why should you not use for...in to iterate arrays?

Why interviewers ask: Classic pitfall that separates beginners from intermediates.

Model answer:

for...in iterates enumerable property keys as strings, which causes three problems for arrays:

  1. Keys are strings, not numbers: "0", "1" -- arithmetic on them causes string concatenation.
  2. Inherited properties from the prototype chain may appear (e.g., if Array.prototype has been extended).
  3. Order is not guaranteed to be numeric in all edge cases.
Array.prototype.custom = function() {};
const arr = ["a", "b"];

for (const key in arr) {
  console.log(key);  // "0", "1", "custom"
}

Use instead: for loop (index control), for...of (values), or forEach/map (functional style).


Q12. How do you create a proper 2D array in JavaScript?

Why interviewers ask: Tests understanding of reference sharing vs independent instances.

Model answer:

Use Array.from() with a factory function to create independent row arrays:

// CORRECT: each row is a new independent array
const matrix = Array.from({ length: 3 }, () => new Array(3).fill(0));
matrix[0][0] = 99;
console.log(matrix);  // [[99,0,0], [0,0,0], [0,0,0]]

// WRONG: fill shares the same reference
const broken = new Array(3).fill(new Array(3).fill(0));
broken[0][0] = 99;
console.log(broken);  // [[99,0,0], [99,0,0], [99,0,0]]

The wrong version fails because fill() puts the same inner array object in every slot. Modifying one row modifies all rows.


Advanced (Q13--Q16)

Q13. How would you implement a merge of two sorted arrays in O(n + m) time?

Why interviewers ask: Classic algorithm question testing two-pointer technique.

Model answer:

Use two pointers, one for each array. Compare elements at both pointers and push the smaller one to the result. After one array is exhausted, append the remainder of the other.

function mergeSorted(a, b) {
  const result = [];
  let i = 0, j = 0;

  while (i < a.length && j < b.length) {
    if (a[i] <= b[j]) {
      result.push(a[i++]);
    } else {
      result.push(b[j++]);
    }
  }

  while (i < a.length) result.push(a[i++]);
  while (j < b.length) result.push(b[j++]);

  return result;
}

mergeSorted([1, 3, 5], [2, 4, 6]); // [1, 2, 3, 4, 5, 6]

Time: O(n + m). Space: O(n + m) for the result array. This is the merge step of merge sort.


Q14. What are the new non-mutating array methods in ES2023?

Why interviewers ask: Shows you keep up with language evolution.

Model answer:

ES2023 introduced change-by-copy methods that return new arrays instead of mutating:

ES2023 methodReplaces (mutating)Behavior
toReversed()reverse()Returns new reversed array
toSorted(fn)sort(fn)Returns new sorted array
toSpliced(start, del, ...items)splice()Returns new array with splice applied
with(index, value)arr[i] = valueReturns new array with one element replaced
const arr = [3, 1, 2];
const sorted = arr.toSorted();
console.log(arr);    // [3, 1, 2] -- unchanged
console.log(sorted); // [1, 2, 3]

const replaced = arr.with(1, 99);
console.log(replaced); // [3, 99, 2]

These are especially valuable in React and other frameworks where immutable updates trigger re-renders.


Q15. Explain how Array.from({ length: n }, mapFn) works.

Why interviewers ask: Tests understanding of array-like objects and factory patterns.

Model answer:

Array.from() can take any array-like object (an object with a numeric length property) and optionally apply a mapping function to each index. { length: n } is a minimal array-like object with n slots.

// Create [0, 1, 2, 3, 4]
Array.from({ length: 5 }, (_, i) => i);

// Create [1, 2, 3, 4, 5]
Array.from({ length: 5 }, (_, i) => i + 1);

// Create 5x5 identity matrix
Array.from({ length: 5 }, (_, r) =>
  Array.from({ length: 5 }, (_, c) => r === c ? 1 : 0)
);

The first argument to the map function is the element (which is undefined for an array-like, hence _), and the second is the index. This is more readable and reliable than new Array(n).fill().map().


Q16. How do you handle the pitfall of fill() with objects/arrays?

Why interviewers ask: Tests deep understanding of references in array initialization.

Model answer:

fill() assigns the same reference to every slot. For primitives this is fine, but for objects or arrays, all slots point to the same object.

// Primitives -- safe
new Array(3).fill(0);         // [0, 0, 0] -- independent

// Objects -- DANGEROUS
new Array(3).fill({});        // [{}, {}, {}] -- same object!
new Array(3).fill([]);        // [[], [], []] -- same array!

// Proof:
const arr = new Array(3).fill([]);
arr[0].push("x");
console.log(arr);  // [["x"], ["x"], ["x"]]

Fix: Use Array.from() with a factory function:

const arr = Array.from({ length: 3 }, () => []);
arr[0].push("x");
console.log(arr);  // [["x"], [], []] -- independent

The arrow function () => [] runs once per slot, creating a fresh array each time.


Quick-fire

#QuestionOne-line answer
1typeof []"object"
2Check for arrayArray.isArray(x)
3Last elementarr.at(-1) or arr[arr.length - 1]
4push() returnsNew length
5pop() returnsRemoved element
6Faster: push or unshift?push -- O(1) vs O(n)
7forEach + break?Cannot break -- use for/for...of
8Clear array in placearr.length = 0
9Copy array (shallow)[...arr] or arr.slice()
10[1,2] === [1,2]false -- different references
11Empty array truthy?Yes -- [] is truthy
12Correct 2D initArray.from({length:n}, () => [])

<- Back to 1.21 -- JavaScript Arrays (README)