in Notes

Note 5. ECMAScript: Unresolved references

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;
}

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
Notice, accessing global variables via properties of the global object in ES2015 is only allowed for the variables created via 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

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!

Write a Comment

Comment

  1. You can test a correct behavior in current version of Node (at the day of writing v.4.x) by executing exactly as a file:

    Ok, maybe we just have different behaviors due to different versions of Node (I’m on 5.4), or maybe there is a simpler explanation, so want to clarify what I meant in Twitter.

    From what I see, Node.js does not implement correct behavior. One of the possible reasons why we’re making different conclusions is that you see error ReferenceError: undeclared is not defined and treat that as correct implementation of mentioned issue.

    However, if you look at the column where the error happens, you can see that it’s unrelated:

    undeclared = (this.undeclared = 10, console.log(undeclared), 10);
                                                    ^
    ReferenceError: undeclared is not defined
    

    What happens here instead is that at the moment of console.log(undeclared) is still not declared, and due to fail mode such reference cannot be resolved. Why does that happen? Because in Node.js code is not executed in the global context, but instead in the context of module itself. So at the moment of calling this.undeclared = 10, in browser this would refer to global object, but in Node it refers to exports instead, so you’re basically assigning exports.undeclared = 10 and next expression console.log(undeclared) fails because global variable undeclared still doesn’t exist.

    Instead, if you change code to

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

    then global binding will be created through property on real global object, thus reflecting the browser behavior, and in that case you get 10 in the output, so now it reflects the initially discussed bug.

  2. @Ingvar Stepanyan, oh, you’re actually right! I completely forgot that this value is Node is usually refers to the evaluating module’s exports, not to the global object. So right, if to use global, then it’s incorrectly makes undeclared available (that basically reflects behavior of Chrome, since V8 is used there too). Thanks, I edited!

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

    Block-scoped variables declared in global code would be bindings of a intermediate scope before the global scope, so this.foo here will be undefined.

  4. @ziyunfei, good catch! True, the Global Environment Record is split on two sub-environments, Object (which is returned as this value in the global context) and Declarative one. And let, const, and class do not affect the object environment. Thanks, I edited to var!