Thinking JavaScript

By  on  

I was teaching a JavaScript workshop the other day and one of the attendees asked me a JS brain teaser during the lunch break that really got me thinking. His claim was that he ran across it accidentally, but I'm a bit skeptical; it might just have been an intentional WTF trick!

Anyway, I got it wrong the first couple of times I was trying to analyze it, and I had to run the code through a parser then consult the spec (and a JS guru!) to figure out what was going on. Since I learned some things in the process, I figured I'd share it with you.

Not that I expect you'll ever intentionally write (or read, hopefully!) code like this, but being able to think more like JavaScript does always help you write better code.

The Setup

The question posed to me was this: why does this first line "work" (compiles/runs) but the second line gives an error?

[[]][0]++;

[]++;

The thinking behind this riddle is that [[]][0] should be the same as [], so either both should work or both should fail.

My first answer, after thinking about it for a few moments, was that these should both fail, but for different reasons. I was incorrect on several accounts. Indeed, the first one is valid (even if quite silly).

I was wrong despite the fact that I was trying to think like JavaScript does. Unfortunately, my thinking was messed up. No matter how much you know, you can still easily realize you don't know stuff.

That's exactly why I challenge people to admit: "You Don't Know JS"; none of us ever fully knows something as complex as a programming language like JS. We learn some parts, and then learn more, and keep on learning. It's a forever process, not a destination.

My Mistakes

First, I saw the two ++ operator usages, and my instinct was that these will both fail because the unary postfix ++, like in x++, is mostly equivalent to x = x + 1, which means the x (whatever it is) has to be valid as something that can show up on the left-hand side of an = assignment.

Actually, that last part is true, I was right about that, but for the wrong reasons.

What I wrongly thought was that x++ is sorta like x = x + 1; in that thinking, []++ being [] = [] + 1 would be invalid. While that definitely looks weird, it's actually quite OK. In ES6, the [] = .. part is valid array destructuring.

Thinking of x++ as x = x + 1 is misguided, lazy thinking, and I shouldn't be surprised that it led me astray.

Moreover, I was thinking of the first line all wrong, as well. What I thought was, the [[]] is making an array (the outer [ ]), and then the inner [] is trying to be a property access, which means it gets stringified (to ""), so it's like [""]. This is nonsense. I dunno why my brain was messed up here.

Of course, for the outer [ ] to be an array being accessed, it'd need to be like x[[]] where x is the thing being accessed, not just [[]] by itself. Anyway, thinking all wrong. Silly me.

Corrected Thinking

Let's start with the easiest correction to thinking. Why is []++ invalid?

To get the real answer, we should go to the official source of authority on such topics, the spec!

In spec-speak, the ++ in x++ is a type of "Update Expression" called the "Postfix Increment Operator". It requires the x part to be a valid "Left-Hand Side Expression" – in a loose sense, an expression that is valid on the left-hand side of an =. Actually, the more accurate way to think of it is not left-hand side of an =, but rather, valid target of an assignment.

Looking at the list of valid expressions that can be targets of an assignment, we see things like "Primary Expression" and "Member Expression", among others.

If you look into Primary Expression, you find that an "Array Literal" (like our []!) is valid, at least from a syntax perspective.

So, wait! [] can be a left-hand side expression, and is thus valid to show up next to a ++. Hmmm. Why then does []++ give an error?

What you might miss, which I did, is: it's not a SyntaxError at all! It's a runtime error called ReferenceError.

Occassionally, I have people ask me about another perplexing – and totally related! – result in JS, that this code is valid syntax (but still fails at runtime):

2 = 3;

Obviously, a number literal shouldn't be something we can assign to. That makes no sense.

But it's not invalid syntax. It's just invalid runtime logic.

So what part of the spec makes 2 = 3 fail? The same reason that 2 = 3 fails is the same reason that []++ fails.

Both of these operations use an abstract algorithm in the spec called "PutValue". Step 3 of this algorithm says:

If Type(V) is not Reference, throw a ReferenceError exception.

Reference is a special specification type that refers to any kind of expression that represents an area in memory where some value could be assigned. In other words, to be a valid target, you have to be a Reference.

Clearly, 2 and [] are not References, so that's why at runtime, you get a ReferenceError; they are not valid assignment targets.

But What About...?

Don't worry, I haven't forgotten the first line of the snippet, which works. Remember, my thinking was all wrong about it, so I've got some correcting to do.

[[]] by itself is not an array access at all. It's just an array value that happens to contain another array value as its only contents. Think of it like this:

var a = [];
var b = [a];

b;  // [[]]

See?

So now, [[]][0], what is that all about? Again, let's break it down with some temporary variables.

var a = [];
var b = [a];

var c = b[0];
c;  // [] -- aka, `a`!

So the original setup premise is correct. [[]][0] is kinda the same as just [] itself.

Back to that original question: why then does line 1 work but line 2 doesn't?

As we observed earlier, the "Update Expression" requires a "LeftHandSideExpression". One of the valid types of those expressions is "Member Expression", like [0] in x[0] – that's a member expression!

Look familiar? [[]][0] is a member expression.

So, we're good on syntax. [[]][0]++ is valid.

But, wait! Wait! Wait!

If [] is not a Reference, how could [[]][0] – which results in just [], remember! – possibly be considered a Reference so that PutValue(..) (described above) doesn't throw an error?

This is where things get just a teeny bit tricky. Hat tip to my friend Allen-Wirfs Brock, former editor of the JS spec, for helping me connect the dots.

The result of a member expression is not the value itself ([]), but rather a Reference to that value – see Step 8 here. So in fact, the [0] access is giving us a reference to the 0th position of that outer array, rather than giving us the actual value in that position.

And that's why it's valid to use [[]][0] as a left hand side expression: it actually is a Reference after all!

As a matter of fact, the ++ does actually update the value, as we can see if we were to capture these values and inspect them later:

var a = [[]];
a[0]++;

a;  // [1]

The a[0] member expression gives us the [] array, and the ++ as a mathematical expression will coerce it to a primitive number, which is first "" and then 0. The ++ then increments that to 1 and assigns it to a[0]. It's as if a[0]++ was actually a[0] = a[0] + 1.

A little side note: if you run [[]][0]++ in the console of your browser, it will report 0, not 1 or [1]. Why?

Because ++ returns the "original" value (well, after coercion, anyway – see step 2 & 5 here), not the updated value. So the 0 comes back, and the 1 gets put into the array via that Reference.

Of course, if you don't keep the outer array in a variable like we did, that update is moot since the value itself goes away. But it was updated, nonetheless. Reference. Cool!

Post-Corrected

I don't know if you appreciate JS or feel frustrated by all these nuances. But this endeavor makes me respect the language more, and renews my vigor to keep learning it even deeper. I think any programming language will have its nooks and crannies, some of which we like and some which drive us mad!

No matter your persuasion, there should be no disagreement that whatever your tool of choice, thinking more like the tool makes you better at using that tool. Happy JavaScript thinking!

Kyle Simpson

About Kyle Simpson

Kyle Simpson is a web-oriented software engineer, widely acclaimed for his "You Don't Know JS" book series and nearly 1M hours viewed of his online courses. Kyle's superpower is asking better questions, who deeply believes in maximally using the minimally-necessary tools for any task. As a "human-centric technologist", he's passionate about bringing humans and technology together, evolving engineering organizations towards solving the right problems, in simpler ways. Kyle will always fight for the people behind the pixels.

Recent Features

  • By
    5 Ways that CSS and JavaScript Interact That You May Not Know About

    CSS and JavaScript:  the lines seemingly get blurred by each browser release.  They have always done a very different job but in the end they are both front-end technologies so they need do need to work closely.  We have our .js files and our .css, but...

  • By
    Creating Scrolling Parallax Effects with CSS

    Introduction For quite a long time now websites with the so called "parallax" effect have been really popular. In case you have not heard of this effect, it basically includes different layers of images that are moving in different directions or with different speed. This leads to a...

Incredible Demos

Discussion

  1. Thanks! Great article!

    The “prefix” variant is a bit more “observable” in the console and adds some scary braces:

      ++([[]][0]) // returns 1
    
    • Sen
      ++[[]][0]

      works just fine!

  2. Jebin

    This is awesome. Reference is the key here.

    And you JavaScript, you never cease to surprise me and I love you for that.

  3. Sen

    Great post. JS is never fails to amaze.

    Regarding member expressions, it seems like the actual value is returned instead of reference when it is used on the RHS of expressions, more like a normal variable.

     var a = [[]];
    var b = a[0];
    a[0]++;
    a; //[1]
    b; // [] //unchanged
    
    • Sen-

      Actually, I think Reference is always returned, but this is distinct from the notion of “reference” we commonly use to describe how non-primitives are held. That is, I think Reference comes back, and then subsequently the value type is reduced, in your example, to the primitive instead of to the reference.

  4. Then why does the follow code return “Uncaught TypeError: undefined is not a function”:

    var a = 0;
    [] = a
    • Because [] = a is an array-destructuring assignment, which expects the thing being destructured (a here) to have an iterator-factory function on it at the special property location Symbol.iterator, and sadly Number objects don’t have a default iterator.

      As a side note, you can do some fun stuff with numbers as iterables if you define an iterator for them, like I show here: https://gist.github.com/getify/49ae9a1f2a6031d40f5deb5ea25faa62

  5. Medvean

    «Note that the expression ++x is not always the same as x=x+1. The ++ operator never performs string concatenation: it always converts its operand to a number and increments it. If x is the string “1”, ++x is the number 2, but x+1 is the string “11”.» JavaScript The Definitive Guide by David Flanagan.
    Recommended reading.

  6. Richard Beam

    [] = [] + 1, works fine in node.js. Why, if [] is not a reference type?

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!