JavaScript (ES2026) Module 6 – Arrays and array methods
Arrays: creating, reading and length
Create JavaScript arrays, read items by index and with at(), learn what length counts, tell holes from undefined and build grids without shared rows.
What you will learn
- Create arrays and read items by index, including negative indexes with at()
- Explain length, holes (sparse arrays) and array-likes
- Represent grids such as seat maps with nested arrays
Before you start
What you’ll learn · Before you start
What you will learn
- Create arrays and read items by index, including negative indexes with at()
- Explain length, holes (sparse arrays) and array-likes
- Represent grids such as seat maps with nested arrays
Before you start
On this page
An array is JavaScript’s ordered list of values: the runs off each ball of an over, the seats in a row of a bus,
the names in a queue. You create one with square brackets, const over = [1, 0, 4, 6, 0, 2], and read an item by its
index, its position counted from 0, so over[0] is the first item and over[5] the sixth. over.at(-1) counts
from the end and reads the last item. The length property is one more than the highest index, 6 here, and it
changes when the array does. Reading a position where nothing is stored gives undefined, never an error. This lesson
runs each of these rules, then looks at what length really counts, at the empty slots called holes, at array-likes
that only look like arrays, and at grids made of arrays inside an array, including a bug that books a whole column of
seats at once.
Create an array and read its items
// The runs scored off each ball of one over of cricket, in the order the six balls were bowled.
const over = [1, 0, 4, 6, 0, 2];
console.log('balls:', over.length);
console.log('first ball:', over[0], '| fourth ball:', over[3]);
console.log('seventh ball:', over[6]);
console.log('last ball:', over.at(-1), '| with length:', over[over.length - 1]);
console.log('over[-1]:', over[-1]);
// The scorer corrects the second ball: it went for 2 runs, not 0.
over[1] = 2;
console.log('after the correction:', over);
let total = 0;
for (const runs of over) total += runs;
console.log('runs in the over:', total); Output
balls: 6 first ball: 1 | fourth ball: 6 seventh ball: undefined last ball: 2 | with length: 2 over[-1]: undefined after the correction: [ 1, 2, 4, 6, 0, 2 ] runs in the over: 15
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node first-array.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The square brackets hold the items, separated by commas, and the array keeps them in the order you wrote them. The
first item is at index 0 and the last at index length - 1, 5 for this over, so over[3] is the fourth ball, the
six. Index 6 is past the end: nothing is stored there, and the read gives undefined without complaint, which is why
a typo in an index often shows up much later as an undefined somewhere else.
To read from the end, at() takes a negative index: at(-1) is the last item and at(-2) the one before it.
at() arrived in ECMAScript 2022 (Node.js 16.6, Chrome 92, Firefox 90 and Safari 15.4 have it); before that, the same
read was written over[over.length - 1], which you will still meet in older code.
Square brackets never count from the end: over[-1] looks for a property named "-1", which an array does not have,
so it gives undefined.
Indexes from the front, and negative positions with at()
Text description of the diagram
The diagram shows the array over, which holds the runs 1, 0, 4, 6, 0 and 2 and has a length of 6.
- Each box is one item, in two rows of three, read left to right. Under each value are two ways to reach it: the index in square brackets, counted from 0 at the front, and the negative position that at() accepts, counted from -1 at the end.
- The first item, 1, is over[0] and over.at(-6). The last item, 2, is over[5] and over.at(-1).
A note under the row says that over[6], over[-1] and over.at(-7) all give undefined, because no item is there. Square brackets never count from the end: over[-1] looks for a property named "-1", which an array does not have.
An index can also stand on the left of =: over[1] = 2 replaces the second item in place, and the array stays the
same array. for…of, from the control-flow module, then walks through the values in index order, which is how the
program adds up the 15 runs of the over.
What length counts
length is not a count of the items you stored. The specification defines it as a number that is always greater than
every index in the array, and the engine keeps that true whenever the array changes:
// length is always one more than the highest index, and writing to length changes the array.
const queue = ['Asha', 'Ravi', 'Meena'];
console.log(typeof queue, Array.isArray(queue), Object.keys(queue));
queue[5] = 'Kiran';
console.log('after queue[5] = ...:', queue.length, queue);
queue.length = 2;
console.log('after length = 2:', queue, queue[2]);
const sameQueue = queue;
queue.length = 0;
console.log('after length = 0:', queue, sameQueue);
const tagged = ['a', 'b'];
tagged.label = 'not an index';
console.log('a non-index key leaves length at', tagged.length);
try {
queue.length = -1;
} catch (error) {
console.log('length = -1 throws', error.name);
} Output
object true [ '0', '1', '2' ] after queue[5] = ...: 6 [ 'Asha', 'Ravi', 'Meena', <2 empty items>, 'Kiran' ] after length = 2: [ 'Asha', 'Ravi' ] undefined after length = 0: [] [] a non-index key leaves length at 2 length = -1 throws RangeError
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node length-rules.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
typeof queueis"object": an array is an object whose property names are its indexes, which is whyObject.keys()lists them as the strings'0','1'and'2'.Array.isArray()is the reliable test for an array, becausetypeofcannot tell an array from any other object.- Writing to index 5 of a three-item array makes
length6. Nothing was stored at indexes 3 and 4, so they are empty slots, shown as<2 empty items>; the next section is about them. - Setting
lengthto a smaller number deletes every item from that index on:queue[2]is gone, not hidden. queue.length = 0empties the array in place.sameQueuenames the same array, so it is empty too. Writingqueue = []instead would make a new, empty array for that one variable and leave every other reference holding the old items; withconst, it is not even allowed.- A property whose name is not an index, such as
tagged.label, does not changelength. Arrays accept such properties because they are objects, butfor…of,forEach()and the other array methods never see them, so keep extra data elsewhere. lengthmust be a whole number from 0 to 4,294,967,295 (2³² − 1). Setting it to a fraction or to a number outside that range, such as -1, throws aRangeError.
Holes: an empty slot is not undefined
An array whose length is larger than the number of items actually stored is sparse, and each missing item is a
hole. A hole is not a slot holding undefined: nothing is stored there at all. The two look alike when you read
them, and most of the time that is all that matters, but array methods treat them differently:
// Holes against undefined values: Array(3) has three empty slots, Array.from({ length: 3 }) three undefined values.
const holes = Array(3);
const values = Array.from({ length: 3 });
console.log(holes, values);
console.log('is index 0 there?', 0 in holes, 0 in values);
let visits = 0;
holes.forEach(() => visits++);
console.log('forEach calls on holes:', visits);
console.log('map on holes:', holes.map(() => 'seat'));
console.log('map on values:', values.map(() => 'seat'));
console.log('spread of holes:', [...holes]);
console.log('JSON of [1, , 3]:', JSON.stringify([1, , 3])); Output
[ <3 empty items> ] [ undefined, undefined, undefined ] is index 0 there? false true forEach calls on holes: 0 map on holes: [ <3 empty items> ] map on values: [ 'seat', 'seat', 'seat' ] spread of holes: [ undefined, undefined, undefined ] JSON of [1, , 3]: [1,null,3]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sparse-and-dense.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Called with a single number, Array(3) (or new Array(3)) makes an array of length 3 with no items in it: three
holes. Array.from({ length: 3 }) makes a dense array of the same length that stores undefined three times.
The in operator tells them apart, since 0 in holes asks whether index 0 exists. From there the methods go their
own ways:
forEach()skips holes, so it called its function 0 times;map()skips them too and keeps the holes in the new array it returns.- Spreading with
[...holes]and looping withfor…ofread every index from 0 tolength - 1, so a hole comes out asundefined. JSON.stringify()writesnullfor a hole, because JSON has neither holes norundefined.
Holes come from Array(n), from writing past the end, from delete list[i] and from a doubled comma in a literal,
[1, , 3], which ESLint’s recommended rule no-sparse-arrays reports. Dense arrays are easier to reason about: create
them with Array.from() or with fill(), and remove items with splice(), which the next lesson covers, rather
than with delete. One more trap of the constructor: only a single number is taken as a length. Array(3, 4) is the
two-item array [3, 4], and Array(2.5) throws a RangeError, because a length must be a whole number.
Array-likes look like arrays but are not
Some objects have a length and items at the indexes 0, 1, 2 and so on, without being arrays. They are called
array-likes:
// Array-likes have a length and index keys, but they are not arrays and have no array methods.
const picked = { 0: 'Mumbai', 1: 'Pune', 2: 'Nashik', length: 3 };
console.log(Array.isArray(picked), picked[1], picked.length, typeof picked.map);
const cities = Array.from(picked);
console.log(Array.isArray(cities), cities.at(-1), cities);
const word = 'chai';
console.log(word.length, word[0], word.at(-1), Array.from(word));
function scores() {
return [arguments.length, Array.isArray(arguments), Array.from(arguments)];
}
console.log(scores(7, 9, 10)); Output
false Pune 3 undefined true Nashik [ 'Mumbai', 'Pune', 'Nashik' ] 4 c i [ 'c', 'h', 'a', 'i' ] [ 3, false, [ 7, 9, 10 ] ]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node array-likes.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
picked has index keys and a length, so picked[1] works, but it has none of the array methods: picked.map is
undefined, and Array.isArray() says false. Strings are array-likes too, and so is the arguments object that
every regular function receives. In a browser you will meet others: document.querySelectorAll() returns a
NodeList, which the DOM module covers. Array.from() copies any array-like, or anything you can loop over with
for…of, into a real array, which then has every method of this module. For a string it gives the characters one by
one, keeping a character such as 😀 in one piece, the same way for…of does.
Grids: arrays inside an array
An item of an array can be another array, and that is how JavaScript holds a table, a game board or the seats of a bus. The outer array holds the rows, and each row holds its seats:
// A bus with 5 rows of 4 seats, A and B on the left of the aisle and C and D on the right.
// Each row is its own array, built by Array.from, so booking a seat changes one row only.
const ROWS = 5;
const LETTERS = 'ABCD';
const seats = Array.from({ length: ROWS }, () => Array(LETTERS.length).fill('open'));
function book(seat) {
const row = Number(seat.slice(0, -1)) - 1; // '3C' → row index 2
const column = LETTERS.indexOf(seat.at(-1)); // 'C' → column index 2
seats[row][column] = 'booked';
}
for (const seat of ['1A', '1B', '3C', '5D']) book(seat);
const mark = (seat) => (seat === 'open' ? '.' : 'X');
console.log(' A B C D');
for (let r = 0; r < seats.length; r++) {
const [a, b, c, d] = seats[r];
console.log(`${String(r + 1).padStart(2)} ${mark(a)} ${mark(b)} ${mark(c)} ${mark(d)}`);
}
let open = 0;
for (const row of seats) {
for (const seat of row) if (seat === 'open') open++;
}
console.log(`${open} of ${ROWS * LETTERS.length} seats are open; seat 3C is ${seats[2][2]}`); Output
A B C D 1 X X . . 2 . . . . 3 . . X . 4 . . . . 5 . . . X 16 of 20 seats are open; seat 3C is booked
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node seat-map.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
seats[2][2] reads in two steps: seats[2] is the third row, and [2] picks its third seat, 3C. The program builds
the grid with Array.from({ length: ROWS }, () => …), which calls the arrow function once for every row and stores
what it returns, so each row is a new array of four 'open' seats. The function also receives the index of the slot
it fills, as its second argument: Array.from({ length: 3 }, (_, i) => i * i) gives [0, 1, 4]. Reading a row that
does not exist gives undefined, and reading a seat of undefined throws a TypeError, so check the row first or
read it with optional chaining, seats[9]?.[0].
The shared-row bug
The shorter way to build a grid looks right and is wrong:
// fill() puts the value it is given in every slot. Given an array, it stores the SAME array three times.
const wrong = Array(3).fill(Array(3).fill('.'));
wrong[0][1] = 'X';
console.log(wrong);
console.log('wrong[0] === wrong[2]:', wrong[0] === wrong[2]);
// Array.from calls its function once per slot, and each call makes a new row.
const right = Array.from({ length: 3 }, () => Array(3).fill('.'));
right[0][1] = 'X';
console.log(right);
console.log('right[0] === right[2]:', right[0] === right[2]); Output
[ [ '.', 'X', '.' ], [ '.', 'X', '.' ], [ '.', 'X', '.' ] ] wrong[0] === wrong[2]: true [ [ '.', 'X', '.' ], [ '.', '.', '.' ], [ '.', '.', '.' ] ] right[0] === right[2]: false
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node shared-row-bug.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
fill(value) stores the value it is given in every slot, and the argument Array(3).fill('.') is evaluated once,
before fill() runs. So the outer array holds the same row three times. Writing an 'X' into wrong[0][1] changes
that single row, and all three rows show it, because there is only one; wrong[0] === wrong[2] is true. With
Array.from(), the arrow function runs three times and makes three rows, and the 'X' stays in row 0:
One shared row, or one row per slot
Text description of the diagram
The diagram has two panels, one above the other.
- The first panel is the grid made with Array(3).fill(Array(3).fill('.')). Its three slots all point to one and the same row array, so writing 'X' into wrong[0][1] puts the X in that single row, and all three rows of the grid show it.
- The second panel is the grid made with Array.from({ length: 3 }, () => Array(3).fill('.')). Array.from calls the arrow function once for each slot, so there are three separate rows. Writing 'X' into right[0][1] changes row 0 only; rows 1 and 2 still hold three dots.
An arrow from the first panel to the second is labelled "the fix".
The same trap waits in Array(n).fill({}) and Array(n).fill([]), which share one object or one array among all the
slots. Copying the grid does not separate the rows either: [...wrong] is a new outer array that holds the same
shared row, three times. In a seat-booking screen this bug books seat B in every row at once, and the exercise below
asks you to build grids that cannot have it.
Exercises
Exercise · Easy · JavaScript
Build a grid without shared rows, then transpose it
Write two functions in grid.mjs. A grid is an array of rows, and each row is an array of the same length.
createGrid(rows, cols, value)returns a new grid withrowsrows ofcolsitems, every item set tovalue.createGrid(2, 3, 0)returns[[0, 0, 0], [0, 0, 0]], andcreateGrid(0, 4, '.')returns[]. Every row must be a separate array: changinggrid[0][0]must leave every other row as it was.transpose(grid)returns a new grid whose rows are the columns ofgrid:transpose([[1, 2, 3], [4, 5, 6]])returns[[1, 4], [2, 5], [3, 6]]. An empty grid gives[], and the grid passed in must not change.
The starter's createGrid() looks right but builds its grid with fill(), and transpose() has no body yet. Run the sample tests when you are done.
Starter code · grid.mjs
// The grid helpers of lesson javascript/arrays-basics. A grid is an array of rows of the same length.
export function createGrid(rows, cols, value) {
// Looks right, but fill() puts the same row array in every slot.
return Array(rows).fill(Array(cols).fill(value));
}
export function transpose(grid) {
// Return a new grid whose rows are the columns of grid.
} The sample tests · grid.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { createGrid, transpose } from './grid.mjs';
test('a 2 by 3 grid of zeros', () => {
assert.deepEqual(createGrid(2, 3, 0), [[0, 0, 0], [0, 0, 0]]);
});
test('every row is a separate array', () => {
const grid = createGrid(3, 2, '.');
grid[0][0] = 'X';
assert.deepEqual(grid, [['X', '.'], ['.', '.'], ['.', '.']]);
assert.ok(grid[1] !== grid[2]);
});
test('a grid with no rows is an empty array', () => {
assert.deepEqual(createGrid(0, 4, '.'), []);
});
test('transposing a 2 by 3 grid gives a 3 by 2 grid', () => {
assert.deepEqual(transpose([[1, 2, 3], [4, 5, 6]]), [[1, 4], [2, 5], [3, 6]]);
});
test('one row becomes one column, and an empty grid stays empty', () => {
assert.deepEqual(transpose([[7, 8, 9]]), [[7], [8], [9]]);
assert.deepEqual(transpose([]), []);
});
test('transpose leaves its input as it was', () => {
const input = [['a', 'b'], ['c', 'd']];
const result = transpose(input);
assert.deepEqual(result, [['a', 'c'], ['b', 'd']]);
assert.deepEqual(input, [['a', 'b'], ['c', 'd']]);
assert.ok(result !== input && result[0] !== input[0]);
}); A hint
Array(rows).fill(row) stores one row in every slot. Array.from({ length: rows }, () => …) calls its function once for every slot, so a function that returns a new row gives each slot a row of its own. In transpose(), the result has one row for each column of grid, and row c of the result holds item c of every row of grid. Mind the empty grid: it has no first row to measure.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the difference between a hole and undefined in a JavaScript array?
A hole is an index where nothing is stored at all, while undefined is a value stored at an index like any other.
Array(3) has three holes, and Array.from({ length: 3 }) stores undefined three times; both have length 3 and
both give undefined when you read an item, so a plain read cannot tell them apart. The in operator can: 0 in Array(3) is false. Methods differ too: forEach(), map() and filter() skip holes, while for…of, spreading
and Array.from() turn them into undefined, and JSON.stringify() writes null. Holes come from Array(n), from
writing past the end, from delete and from a doubled comma in a literal. Avoid them by creating arrays with
Array.from() or fill().
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A class register stores marks with marks[rollNumber] = score, and the roll numbers are 101, 205 and 340. The report prints "Class size: 341", the average is wrong, and JSON.stringify(marks) is mostly null. Explain what happened to the array, and what you would store instead.
Key takeaways
[a, b, c]creates an array;list[i]reads the item at indexi, counted from 0, andlist.at(-1)reads the last item. A read where nothing is stored givesundefined.lengthis one more than the highest index. Writing past the end grows it and leaves holes; setting it smaller deletes items, andlist.length = 0empties the array for every reference to it.Array.isArray()tells an array from other objects;typeofgives"object"for both.Array(3)makes three holes, whichforEach()andmap()skip;Array.from({ length: 3 })makes three realundefinedvalues.- Array-likes such as strings,
argumentsand aNodeListhave alengthand index keys but no array methods;Array.from()turns them into arrays. - Build grids with
Array.from({ length: rows }, () => Array(cols).fill(value)).Array(rows).fill(row)stores one row in every slot, and a change to one seat shows in every row.
References
- ECMAScript 2026 Language Specification: Array Exotic Objects (Ecma International)
- ECMAScript 2026 Language Specification: ArraySetLength (Ecma International)
- ECMAScript 2026 Language Specification: The Array Constructor (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.at (Ecma International)
- ECMAScript 2026 Language Specification: Array.from (Ecma International)
- ECMAScript 2026 Language Specification: Array.prototype.fill (Ecma International)
- Array (MDN Web Docs)
- Array.prototype.at() (MDN Web Docs)
- Array.from() (MDN Web Docs)
- ESLint rule: no-sparse-arrays (ESLint (OpenJS Foundation))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress