An array's length counts slots, and a slot can be empty: no element at all, not even undefined. That empty slot is called a hole, and an array with holes is called sparse. Reading a hole gives undefined, the value you get for any property that doesn't exist, so it looks like an element holding undefined. Object.keys, which lists the indexes an array really has, tells the two apart:
const withHole = [1, , 3]; // nothing between the commas
const withUndefined = [1, undefined, 3];
withHole.length; // 3
withUndefined.length; // 3
withHole[1]; // undefined
withUndefined[1]; // undefined
Object.keys(withHole); // ["0", "2"]: there's no element at index 1
Object.keys(withUndefined); // ["0", "1", "2"]: there is one, and its value is undefined
Holes appear in a few ways:
| How | Example | Result |
|---|---|---|
| A gap in an array literal | [1, , 3] | A hole at index 1 |
new Array with one number | new Array(3) | Three holes, and length is 3 |
delete on an element | delete arr[1] | Index 1 becomes a hole, and length doesn't change |
| Writing past the end | arr[5] = 6 on [1, 2, 3] | Holes at indexes 3 and 4, and length is 6 |
Making length bigger | arr.length = 5 on [1, 2, 3] | Holes at indexes 3 and 4 |
Consoles mark holes so you can spot them: Chrome shows [1, , 3] as [1, empty, 3], and Node as [ 1, <1 empty item>, 3 ].
Built-in methods don't agree on what to do with a hole: some skip it, some keep it in their result, and some read it as undefined, or write null in JSON. That's why every task that rebuilds one of them says exactly what to do with holes. Here's [1, , 3] going through a few:
| Call | Result | What happened to the hole |
|---|---|---|
[1, , 3].filter(() => true) | [1, 3] | Skipped: the callback isn't called for it, and the result has no hole |
[1, , 3].map((n) => n * 10) | [10, , 30] | Skipped, but kept: the result has a hole in the same place |
[1, , 3].reduce((sum, n) => sum + n) | 4 | Skipped: 1 starts the total, and the callback runs once, to add 3 |
[1, , 3].flat() | [1, 3] | Dropped |
Array.from([1, , 3]) | [1, undefined, 3] | Read as undefined, which becomes a real element |
JSON.stringify([1, , 3]) | "[1,null,3]" | Written as null |
Write zip(...arrays). It takes any number of arrays and returns an array of tuples (small arrays): tuple i holds item i of each array, in the order the arrays were passed.
The result is as long as the longest array. Where a shorter array has run out, its place in the tuple holds undefined, as a real entry, not a hole. With no arrays, or only empty ones, zip returns [].
Write unzip(tuples), which does the reverse: it takes an array of tuples and returns one array per position. Tuples may have different lengths; unzip pads with undefined the same way, so the result has as many arrays as the longest tuple has items.
Neither function changes its input.
zip([1, 2, 3], ["a", "b", "c"]); // [[1, "a"], [2, "b"], [3, "c"]]
zip([1, 2], ["a"], [true]); // [[1, "a", true], [2, undefined, undefined]]
zip(); // []
unzip([[1, "a"], [2, "b"]]); // [[1, 2], ["a", "b"]]
unzip([[1, "a", true], [2]]); // [[1, 2], ["a", undefined], [true, undefined]]
expect(zip([1, 2, 3], ["a", "b", "c"])).toEqual([[1, "a"], [2, "b"], [3, "c"]]);
expect(zip([1, 2], [3, 4], [5, 6])).toEqual([[1, 3, 5], [2, 4, 6]]);
expect(zip(["a", "b"])).toEqual([["a"], ["b"]]);
expect(zip([1, 2, 3], ["a"])).toEqual([[1, "a"], [2, undefined], [3, undefined]]);
expect(zip(["a"], [1, 2, 3])).toEqual([["a", 1], [undefined, 2], [undefined, 3]]);
const tuples = zip([1, 2], ["a"]);
expect(tuples[1]).toHaveLength(2);
expect(1 in tuples[1]).toBe(true);
const [first] = zip([], [1]);
expect(first).toHaveLength(2);
expect(0 in first).toBe(true);
expect(zip()).toEqual([]);
expect(zip([], [])).toEqual([]);
expect(unzip([[1, "a"], [2, "b"], [3, "c"]])).toEqual([[1, 2, 3], ["a", "b", "c"]]);
expect(unzip([])).toEqual([]);
const ids = [1, 2, 3];
const names = ["Ada", "Lin", "Cy"];
const admins = [true, false, true];
expect(unzip(zip(ids, names, admins))).toEqual([ids, names, admins]);
expect(unzip([[1, "a", true], [2]])).toEqual([[1, 2], ["a", undefined], [true, undefined]]);
const result = unzip([[1], [2, "b"]]);
expect(result).toEqual([[1, 2], [undefined, "b"]]);
expect(0 in result[1]).toBe(true);
const a = [1, 2];
const b = ["x"];
zip(a, b);
expect(a).toEqual([1, 2]);
expect(b).toEqual(["x"]);
const tuples = [[1, "a"], [2]];
unzip(tuples);
expect(tuples).toEqual([[1, "a"], [2]]);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with zip and unzip. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.