loading...

new Array(1) => [empty title x 1]

alephnaught2tog profile image M. Shemayev Updated on ・8 min read

Imagine it's been a rough week.

Finally, it's time to refill our weekly chocolate supply. As usual, we're using JavaScript to fill our chocolate supply.

In pseudocode, "a box of 21 of the same kind of chocolate" would be something like:

  1. Get a box.
  2. Make sure the box has enough slots for 21 chocolates.
  3. As long as there are still empty slots:
    • Get a chocolate from the giant bag of chocolates.
    • Put one chocolate into the slot you're looking at.
    • Move on to the next slot.

Pretty reasonable, right? Let's give it a shot!

(Note: all snippets should be runnable as-is in a repl or console as desired, just by copy-pasting if you like.)

Attempt 1: .map

For a first swing, maybe we'd try map:

let chocolate = {
    kind: 'dark',
    filling: 'raspberry ganache'
};

// Prep our box and make sure it has 21 slots
let weeklyChocolateSupplyBox = new Array(21);

// Put one chocolate into every slot
weeklyChocolateSupplyBox.map(_emptySlot => chocolate);

(If you're wondering about the underscore (i.e., _emptySlot), that means the variable is either unimportant or unused. Some languages enforce that as a rule, like Elixir; here, it's purely convention.)

So far, so good: we make an array with 21 slots, we loop over it with map, and put a chocolate in at each slot.

We actually put the exact same chocolate in each slot, which would be less than ideal in the real world — any changes to any one chocolate would effect EVERY chocolate, as they are all this way the same chocolate.

(╯°□°)╯︵ [Ɛ x ʎʇdɯǝ]

Maybe not too surprisingly, it doesn't work. Instead of an array containing 21 identical chocolates, if you run that snippet in a console, you'll get something like: [empty × 21].

Less than ideal, to say the least.

Attempt 2: for (let index ... )

While I love using the various array methods when I can — e.g., forEach, filter, map, etc., I've found that since I learned C-style for loops first, I often refer back to them when things aren't working. Similarly, as a sanity check, I often log out something before and after the loop, so I can make sure nothing truly whacky is going on like being in the wrong file, etc.

At the end of the day, a loop is a loop, use what is clearest to you and others.

So, we try again!

// same as before
chocolate = {
    kind: 'dark',
    filling: 'raspberry ganache'
};

// assign the variable a whole new array to reset.
weeklyChocolateSupplyBox = new Array(21);

console.log('before loop');
for (let index = 0; index < weeklyChocolateSupplyBox.length; index += 1) {
    console.log('loop number %d', index);
    weeklyChocolateSupplyBox[index] = chocolate;
}

console.log(weeklyChocolateSupplyBox);
console.log('after loop');

This time, we succeed. We have a box with 21 chocolates in it, as desired! Awesome.

Attempt 3: for ... of

Say that I didn't use an old-school for loop: say I had gone ahead with a for ... of loop — after all, I want to loop over this array and put things into it, right? This way, too, I can eliminate needing to increment the index myself, and not worry about if I forgot a condition or something. Great!

So let's write the code, and use a for ... of loop instead. We start off the same as before, and sketch out the skeleton of our for loop.

chocolate = {
    kind: 'dark',
    filling: 'raspberry ganache'
};

// assign the variable a whole new array to reset.
weeklyChocolateSupplyBox = new Array(21);

console.log('before loop');
for (let emptySlot of weeklyChocolateSupplyBox) {
    console.log('emptySlot', emptySlot);
    // Put a chocolate into our emptySlot
}
console.log('after loop');

...but what goes inside the loop? We have an emptySlot — but no way to add a chocolate to it. If we ran this now, we'd just see emptySlot undefined logged out 21 times. Not helpful.

Attempt 4: for ... in

In JavaScript, everything is an object. Arrays are too — in particular, are an object created by the Array constructor. By definition, they have a length property, and numeric, ordered keys.

There's another kind of for loop we haven't tried: for ... in, which loops over the properties of an object. For something like an object literal, it loops over the property names; for an array, it loops over the indices. A little weird, but if you think about it, that seems sort of reasonable — we can use both a string key and an array index to set the value, and then later access that value by the key, right?

const dog = { name: 'Simon', age: 13, weight: 50 };
const someNumbers = [3, 1, 4];
for (let key in dog) {
    console.log('dog key', key); // 'name', then 'age', then 'weight'
    console.log('dog value', dog[key]); // 'Simon', then 13, then 50
}

for (let key in someNumbers) {
    console.log('someNumbers key', key); // '0', then '1', then '2'
    console.log('someNumbers value', someNumbers[key]); // 3, then 1, then 4
}

Okay, cool, nothing too interesting there, except for maybe being able to do that with arrays as well.

So, let's try the chocolate experiment again. The normal for loop worked — let's try the same thing but with a for ... in loop, and we can use the index to add it to the array like before.

chocolate = {
    kind: 'dark',
    filling: 'raspberry ganache'
};

// assign the variable a whole new array to reset.
weeklyChocolateSupplyBox = new Array(21);

console.log('before loop');
for (let emptySlotIndex in weeklyChocolateSupplyBox) {
    console.log('emptySlotIndex', emptySlotIndex);
    weeklyChocolateSupplyBox[emptySlotIndex] = chocolate;
}
console.log('after loop');

This time, we see before loop and after loop, and ... literally nothing else.

What's the difference?

So, we tried a number of things:

  • map: failed -- did nothing
  • for ... of loop: failed -- no way to add a chocolate
  • for ... in loop: failed -- never even looped!
  • basic for loop: worked!

None of this answers the question, though: why does a for loop work and the other options fail, with for ... in never looping?

The answer lies in the specification of JavaScript itself.

The Array constructor does create an Array object and set its length to be the (single, numeric) value given1.

What it does not do, though, is set the indices (which are just keys, remember, which happen to be numbers) on the array object.

// This is about what happens:
const newArray = {
    length: 2
};

// NOT this:
const badNewArray = {
    length: 2,
    '0': undefined,
    '1': undefined
};

If you've ever tried to remove something from an object — truly get rid of it, not just give it an undefined value, but remove the property entirely — you know that chocolate['filling'] = undefined won't cut it. The property will still be there, just with undefined as its value.

To remove a property, you have to delete it: delete chocolate['filling'];. After that, if you inspect the object, there will be no key called filling present. If we looked at its keys, we would not see filling listed.

So, what happens if you delete an index from an array?

const someOtherArray = ['value at 0', 'value at 1', 'value at 2'];

console.log(someOtherArray); // ["value at 0", "value at 1", "value at 2"]
console.log(someOtherArray.length); // => 3

delete someOtherArray[1];

console.log(someOtherArray.length); // => still 3
console.log(someOtherArray);
// Chrome:  ["value at 0", empty, "value at 2"]
// Firefox: ["value at 0", <1 empty slot>, "value at 2"]
// Safari:  ["value at 0", 2: "value at 2"]

Each browser shows you the same thing, just differently: an array with length three and only two things in it, at 0 and 2. There's nothing at index 1 anymore — because there is no index 1. Each array still has a length of 3.

This explains why for ... in failed so badly: the for ... in loop works over the keys of an object: there were no keys (indices) for it to enumerate over. Similarly, if we had looped above, both before and after deleting the index, we would have gone into the loop 3 times before deleting the index, and twice after its deletion.

A not-so-well-known symbol

Here's another clue: [...new Array(3)] does what we had probably originally expected — and gives us [undefined, undefined, undefined].

The answer is iterators; specifically, the value of the Symbol.iterator on an object. (Symbols are a JavaScript primitive whose value is unique, and are often used as identifiers — much like atoms in other languages.)

If an object has a Symbol.iterator, that object is iterABLE: it has an iterATOR, an object that adheres to the iterator protocol. Iterators are very neat and very powerful — they're the guts behind async, await, generators, promises, the spread operator, for ... of, etc; they allow for entering and exiting different execution contexts asynchronously.

For our purposes, though, it's enough to know that an iterator essentially keeps track of your place in a loop. Many JavaScript objects have a default iterator — arrays, as well as anything else that you can spread (use ... as above).

In particular, the default iterator specification2 says something like:

  • Start with index = 0.
  • As long as index is less than array.length:
    • Yield the value of array[index]
    • Increment the index, index += 1
    • Keep going

Lots of other array methods use similar logic — e.g., toString uses join, which has a similar algorithm.

What do you get when you access a property that isn't on an object? In some languages, it wouldn't compile at all; in JavaScript, however, you don't get an error, you just get undefined — which, of course, can also be the value if the key is there.

const withKeyAndUndefined = { apples: undefined, pears: 3 };
const withKeyAndValue = { apples: 12, pears: 99 };
const withoutKey = { pears: 74 };

console.log(withKeyAndUndefined['apples']); // => undefined
console.log(withKeyAndValue['apples']);     // => 12;
console.log(withoutKey['apples']);          // => undefined

As for map failing as well?

Well... The specification3 for map (and forEach and other similar methods) spells out that the callback given is only executed for those values "which are not missing" — that is, non-empty slots or where the indices are defined (so, nowhere right after construction).

const yetAnotherArray = new Array(5);

yetAnotherArray.map(
    value => {
        throw new Error('never gonna happen');
    }
).fill(
    null // now we put something in every spot
).map(value => {
    console.log('now, this will show "null": ', value);
    return value;
});

Meanwhile, our basic for-loop worked right off the bat: because we were creating those indices by setting a value under that key, the same way I can do const dog = {name: 'Simon'}; dog.favoriteFood = 'peanut butter'; without favoriteFood ever having been defined as being on the original object.

const array = new Array(5);

for (let index = 0; index < array.length; index += 1) {
    // does 'index' exist? Yes! It's its own variable, after all
    console.log('index', index);
    console.log(`before: ${index} in array?`, index in array);
    array[index] = 'whee';
    console.log(`after: ${index} in array?`, index in array);
}

There is, conveniently, a method to fill an array with any value. We can use that here, too.

For a simple case, we can just do new Array(5).fill(chocolate); for anything more complex, though, we need first to fill the array with something — anything, even null or undefined.

weeklyChocolateSupplyBox = new Array(21).fill(chocolate);
console.log(weeklyChocolateSupplyBox);

const rangeFrom_1_to_10 = new Array(10).fill(null).map((_null,index) => index + 1);

console.log(rangeFrom_1_to_10);

Remember, though, that what we actually end up with here is 21 references to the same chocolate — if we melt one chocolate, they all melt, as what we really did was put the same identical chocolate into every slot through some truly spectacular quantum confectionary. (Chocolate, however, seemed much more enjoyable than an array of strings or numbers.)


  1. Array constructor specification
  2. Iterator specification
  3. map specification

Discussion

markdown guide
 

I think I've come across that issue with mapping over an array of empty slots before and I didn't have the time to dig in and understand it. Thanks for this post!