⭐️ Donate Support this site If you like this work and find it useful, consider donating to support ads-free and high-quality education.

Recently an old playing ES5 bug was raised on Twitter, and as the poll shown, sometimes results can be very fun 🙂

Let’s talk about References.

We’ve got familiar with the Reference specification type before, there are also very good explanations around the internets. However, let’s briefly recall when this type comes into play, and how confusing it can be in some cases (luckily such cases shouldn’t appear in practice that often).

A reference is usually involved when we refer to an identifier:

'use strict'; var foo = 10; // Reference `foo` foo;

As a result of evaluation of this expression, which happens during the process called identifier resolution, we get the resolved reference:

var fooReference = { base: global, referencedName: 'foo', strict: true, };

Basically, it’s a “boxed” value, so we can change it if pass the reference to some function:

fooReference.base[fooReference.referencedName] = 20; console.log(foo); // 20

In case if the base object doesn’t have such property, the base component in returned as undefined:

'use strict'; bar;

The reference for bar identifier is called an unresolved reference:

var barReference = { base: undefined, referencedName: 'bar', strict: true, };

Notice, the strict component of a reference determines whether the reference was resolved in a strict code, i.e. the code which is evaluated in the strict mode.

From the strict mode analysis we know that assignment to an undeclared variable throws a ReferenceError . So in our case, being in the strict mode, and trying to assign to bar , results to an error:

bar = 5; // ReferenceError: "bar" is not defined

Practical implication for this is of course to protect from accidental assignments, and pollution of the global scope which such assignments may cause. In a non-strict (“sloppy”) mode, it’s possible:

function foo() { bar = 10; } foo(); console.log(bar); // oops, 10, escaped to global

We also know that this value in the global context refers to the global object, and that we can access global variables as properties of the global object:

'use strict'; var foo = 10; console.log(this.foo); // 10

var keyword. The let , const , and classes do not create properties. var foo = 10; console.log(this.foo); // 10 let bar = 20; console.log(this.bar); // undefined Notice, accessing global variables via properties of the global object in ES2015 is only allowed for the variables created viakeyword. The, and classes do not create properties. This is due the Global Environment Record in ES2015 is split into two parts: Object Environment Record (which is returned as this value in the global context), and Declarative Environment Record. Only functions and variables created with var affect the Object Environment Record.

And first related thing to it, is while not being able to assign to an undeclared identifier, we can normally assign to a new non-existing property of the global object:

'use strict'; this.baz = 10; console.log(baz); // OK, 10 // Can normally reassign baz = 20; console.log(baz); // 20

One interesting subtle part involved with strict mode and unresolved references, is a combination in one statement of these two features: assignment to an “undeclared” identifier, after assignment to a new property:

'use strict'; undeclared = (this.undeclared = 10, console.log(undeclared), 10);

What’s the result of this assignment? Is undeclared now 10 ? By logic which we have seen above, yes, it should be 10 . However, ES spec had a “bug” since ES5 (which likely was considered a feature, and wasn’t fixed in ES6), and this example actually throws ReferenceError . Let’s trace what’s happening there.

The algorithm is as follows:

First we evaluate LHS (left-hand side), which is… an unresolved reference, since undeclared hasn’t been declared yet!

var undeclaredReferecne = { base: undefined, referencedName: 'undeclared', strict: true, };

Next we evaluate the RHS, which is a sequence expression wrapped into the grouping operator: it evaluates each sub-expression, and returns final as the result. So first sub-expression basically normally defines a global property undeclared with the value 10 (which as we have seen above should be accessible after it):

this.undeclared = 10;

The second sub-expression correctly outputs its value:

console.log(undeclared); // 10

As we see, we again resolve the undeclared identifier, and at this time it’s already returned as resolved:

var undeclaredReferecne = { base: global, referencedName: 'undeclared', strict: true, };

The last sub-expression just returns 10 as the final result of the group expression, and this result should be assigned now to the first reference (the LHS) we resolved. And according to the 6.2.3.2, step 5.a.i, we should throw ReferenceError , since we already got an unresolved reference on LHS.

At first glance, this doesn’t make much sense, and we could say it was an editorial error in the spec. However, it is still in the latest ES2015 (aka ES6 spec), and a correct behavior is an implementation should throw the ReferenceError here. A rationale for this, is to avoid possible side effects (like in this specific case), which may appear after evaluating first RHS. So the spec decided to standardize it as “always left to right” evaluation order, and it’s been that way since ES1.

In ES2015 though, “left-to-right” order relates only to a simple assignment. In case of destructuring assignment, RHS is evaluated first:

'use strict'; var {x} = (this.x = 1, {x}); console.log(x); // 1

Note, at the moment seems none of the implementations handle simple assignment case per spec.

The implementations though, as was pointed out, still evaluate correctly LHS first. This can be shown on the following example, when console.log(1) is executed first:

var foo = {}; // LHS // RHS (console.log(1), foo).prop = (console.log(2), 10); console.log(foo); // {prop: 10}

However, in case of our initial assignment example, they seem cause this side effect, when the reference becomes resolved (even though LHS was determined as unresolved first, its state might be updated during evaluation of the RHS), and the global variable becomes already defined.

Have fun with ES and implementing of programming languages in general!