Sign in

Libre University uses your GitHub account. Signing in is only needed to sit a final test, so the score is kept on your profile.

Arrays

A function that averages three numbers can take three parameters, and a function that averages an unknown number of them cannot be written at all with what this course has so far.

An array is one value that holds many values, in order, each reachable by its position. It is the first data structure, it is the one you will use most, and almost everything in the rest of this course is either an array or something built out of arrays. If you have arrived here directly, what is assumed is const and let, a for loop, and a function that returns a value.

Many values, one name

An array is written as a comma separated list in square brackets:

const scores = [72, 85, 91, 60, 78];
const names = ["ana", "ben", "cleo"];
const mixed = [1, "two", true, null];

Nothing requires the elements to share a type, as the third line shows, and nothing recommends it either: an array whose elements are all the same kind of thing is far easier to write a loop over, and mixing types is usually a sign that an object, two lessons ahead, is the right shape instead.

An empty array is [], and it is a perfectly good value: it is the correct answer to "which of these customers owe money" when none do, and it is the right thing to start an accumulator at when you are building a list. Note that typeof [] returns "object", which is unhelpful, so the test for an array is Array.isArray(x).

Positions start at zero

Reach an element by writing its position in square brackets:

const scores = [72, 85, 91, 60, 78];
console.log(scores[0]);   // 72
console.log(scores[2]);   // 91

The first element is at index 0, not 1. This is not an arbitrary cruelty. An index is an offset from the start of the block of memory the array occupies, so the first element sits zero places along, and the address of element i is the address of the array plus i times the size of an element. Zero based indexing makes that arithmetic exact, which is why C chose it and why nearly every language since has followed. The half open loop convention from the loops lesson exists for the same reason, and the two fit together: for (let i = 0; i < scores.length; i++) visits every element exactly once.

scores.length is the number of elements, 5 here, so the last valid index is length - 1. Reading scores[5] is the classic off-by-one, and JavaScript's response to it is one of its more dangerous choices: it does not raise an error, it returns undefined. In C the same read gives whatever byte happened to be there; in Python it raises IndexError and stops; in JavaScript your program carries on with undefined and fails four functions later, somewhere that has nothing to do with the mistake. Reach the last element with scores[scores.length - 1], and remember the symptom: an unexplained undefined is very often an index one too large.

Writing to an index works the same way, scores[0] = 100, and writing past the end extends the array rather than complaining. scores[8] = 1 on a five element array gives it a length of 9 with three empty slots in the middle, which is almost never what anybody wanted.

Growing and shrinking

Arrays are not fixed in size. Four methods change the length, and they come in a pair at each end.

push(v) adds v to the end and returns the new length. pop() removes the last element and returns it. Together they make an array into a stack, which is the structure behind undo histories and the call stack this course meets in the errors lesson.

const stack = [1, 2, 3];
stack.push(4);          // stack is [1, 2, 3, 4], returns 4
const last = stack.pop();  // last is 4, stack is [1, 2, 3]

unshift(v) and shift() do the same at the front. They are correct and they are not free: adding or removing at the front means moving every other element along one place, so the cost grows with the length of the array while push and pop do not. On a five element array that is invisible. Inside a loop over a million element array it is the difference between a second and an afternoon, and the following course on algorithms is largely about learning to see that difference before it bites.

All four mutate the array they are called on, which is to say they change it in place rather than returning a changed copy. That is worth flagging now because the string methods of the first lesson did the opposite, and array methods split into these two camps in a way that has to be memorised. push, pop, shift, unshift, splice, sort and reverse mutate. slice, concat, join, indexOf and everything in the next lesson do not.

That difference is why const on an array is not a contradiction. const scores = [...] forbids rebinding the name to a different array; it says nothing about the contents, and scores.push(9) on a const array is legal. Exactly why that is so, and what it costs you, is the lesson on references.

Walking an array

The counting loop of the earlier lesson turns into a walk over data with one change, using i as an index rather than as a value:

function mean(values) {
  if (values.length === 0) return null;
  let total = 0;
  for (let i = 0; i < values.length; i++) {
    total = total + values[i];
  }
  return total / values.length;
}
console.log(mean([72, 85, 91, 60, 78]));
// 77.2

This is the shape to have in your fingers. The accumulator is declared before the loop, the loop covers 0 to length - 1 inclusive, and the answer is read afterwards. The invariant is the one from the loops lesson with the array substituted in: before each test, total holds the sum of the first i elements. Establishment is total = 0 with i = 0, and at termination i is values.length, so total is the sum of all of them.

The guard on the first line is not decoration. The mean of nothing is not zero, it is undefined, and without the guard the function returns 0 / 0, which is NaN, which then contaminates every number computed from it. Empty is the input everybody forgets, and it is worth making a habit of asking what your function does with [] before you consider it finished.

Where the index itself is not needed, for (const v of values) reads better and cannot be given a wrong bound:

for (const v of values) {
  total = total + v;
}

Beware of for (const i in values), which is a different statement: it iterates over the keys, and for an array those keys are the strings "0", "1", "2". Using it to sum an array concatenates text instead. Use of for values, and for arrays essentially never use in.

Example. What does this print, and what is wrong with it?

const xs = [4, 8, 15];
let total = 0;
for (let i = 0; i <= xs.length; i++) {
  total += xs[i];
}
console.log(total);

It prints NaN. The condition is <=, so i takes the values 0, 1, 2 and 3. There is no element at index 3, so xs[3] is undefined, and 27 + undefined is NaN. Nothing reports an error at the moment of the bad read: the failure appears only in the final value, and it appears as a number that is not a number. Changing <= to < fixes it.

Now you. What does last hold, and what should the code have said?

const xs = [4, 8, 15, 16];
const last = xs[xs.length];
Answer

last is undefined. xs.length is 4 and the valid indices are 0 to 3, so index 4 is one past the end. The correct expression is xs[xs.length - 1], which is 16. This is the same off-by-one as the loop above, and it produces the same silent undefined rather than an error.

Finding things

Searching an array means walking it until you find what you want, and the loop is worth writing by hand once before using the built-in:

function indexOfValue(values, wanted) {
  for (let i = 0; i < values.length; i++) {
    if (values[i] === wanted) return i;
  }
  return -1;
}

Two decisions in four lines. Returning as soon as a match is found means the loop stops early, so the cost depends on where the match is rather than on the length of the array. And -1 is the answer for "not present", chosen because it is not a valid index and therefore cannot be confused with one. That convention is old and universal, and it has a matching hazard: -1 is truthy, so if (indexOfValue(xs, v)) is true when the value is absent and false when it is at index 0, which is precisely backwards. Test !== -1 explicitly.

The built-in versions are values.indexOf(wanted), identical to the above, and values.includes(wanted), which returns a boolean and is what you want when the position does not matter. They differ in one corner: indexOf compares with ===, so it can never find NaN, while includes uses a comparison that can. [NaN].indexOf(NaN) is -1 and [NaN].includes(NaN) is true.

While on built-ins, one trap deserves its own paragraph because it is the most surprising default in the language. sort() with no argument converts every element to a string and sorts those:

console.log([10, 9, 1, 2].sort());
// [1, 10, 2, 9]

That is correct alphabetical order for "1", "10", "2", "9", and it is nonsense as an order for numbers. To sort numbers you must supply a comparison function, which takes two elements and returns a negative number, zero or a positive number:

console.log([10, 9, 1, 2].sort((a, b) => a - b));
// [1, 2, 9, 10]

sort also mutates the array it is called on, so sorting inside a function changes the caller's array. [...values].sort((a, b) => a - b) sorts a copy and leaves the original alone.

Example. What is wrong with this check, and what does it do for each of the three names?

const names = ["ana", "ben", "cleo"];
if (names.indexOf("ana")) console.log("found");

indexOf("ana") returns 0, which is falsy, so nothing is printed even though the name is there. For "ben" it returns 1, which is truthy, so it prints; for "dave" it returns -1, which is also truthy, so it prints for a name that is absent. The test is right for exactly one of the three cases and wrong for the other two. Write if (names.indexOf("ana") !== -1), or better if (names.includes("ana")).

Now you. const ages = [30, 9, 100, 25]; What does ages.sort() give, and what is needed to sort them as numbers?

Answer

ages.sort() gives [100, 25, 30, 9], because the elements are compared as the strings "100", "25", "30", "9" and "1" sorts before "2". A comparison function is needed: ages.sort((a, b) => a - b) gives [9, 25, 30, 100]. Both calls also reorder ages itself, so if the original order mattered, sort a copy.

Building a new array from an old one

The other basic shape, alongside accumulating a single value, is producing a new array. The accumulator starts as [] and the body pushes:

function evens(values) {
  const out = [];
  for (const v of values) {
    if (v % 2 === 0) out.push(v);
  }
  return out;
}
console.log(evens([1, 2, 3, 4, 5, 6]));
// [2, 4, 6]

Building a new array rather than deleting from the old one is worth adopting as a default. Removing elements from an array while looping over it is a classic source of skipped elements: splice shifts everything after the removal down by one, so the index that was about to be visited now holds a value that has never been seen, and the loop steps straight past it. Building fresh has no such problem, and the original stays available if it turns out you needed it.

Example. Write a function returning the largest value in an array, with null for an empty one. What is the invariant, and what does it give for [3, 9, 2]?

function largest(values) {
  if (values.length === 0) return null;
  let best = values[0];
  for (let i = 1; i < values.length; i++) {
    if (values[i] > best) best = values[i];
  }
  return best;
}

The invariant is that before each test, best holds the largest of the first i elements. It is established by taking values[0] and starting at i = 1, which is why the loop starts at 1 rather than 0. For [3, 9, 2]: best starts at 3, becomes 9, and 2 does not beat it, so the answer is 9. Note that starting best at 0 instead would be a bug, giving 0 for an array of negative numbers.

Now you. Write a function that returns a new array of the squares of the values it is given, and say what it returns for [].

Answer
function squares(values) {
  const out = [];
  for (const v of values) {
    out.push(v * v);
  }
  return out;
}

For [] the loop body never runs and it returns [], which is the right answer: the squares of no numbers are no numbers. Unlike mean, this function needs no guard, because the empty case falls out correctly. Deciding which of the two situations you are in is part of writing any function over an array.

Three shapes, over and over

Look at the three loops in this lesson side by side. mean walks the array and combines everything into one value. evens walks it and keeps some of the elements. squares walks it and replaces each element with something computed from it. Every array loop you write for the rest of your life will be one of those three, or a combination of them, and the proportion is not close: they cover the great majority of real code.

Because they are so common they have names, and methods that say the name in the first word instead of leaving the reader to work it out from the tenth line. That is the next lesson.