JavaScript Corners – Part 6

JavaScript Corners – Part 6

In what order does the following evaluate?

a()[b()] = c()[d()] = e()[f()];

TL;DR Answer

get a
call a
get b
call b
get c
call c
get d
call d
get e
call e
get f
call f
get e.f
set c.d
set a.b

Step 1: Variable Access

First off, what does this code even mean? If you’re not intimate with JavaScript, this might seem like a very confusing line of code. In fact, even if you’re familiar with JavaScript, this can be confusing.

So let’s break it down, starting with:

a

The expression a loads the value a from the surrounding scope1. This is done by searching up the scope chain until a is found.

There are a number of different types of scopes in JavaScript, including those that refer to blocks (like the inside of a for-loop), functions (the contents of a function), objects (scopes that are created using a with statement, or the global scope).

For our purposes, let’s define a at the global scope. You’ll see why in a moment. Assuming we’re working in Node.js, the global object is called global, and properties of the global object are part of the global scope2.

global.a = 42;
console.log(a); // prints 42

But, since we’re interested in the order of evaluation, it would be useful to know when the value a is accessed. Luckily, in JavaScript, you can define properties that have a getter and/or setter, which we can use to log when the global variable is accessed:

Object.defineProperty(global, 'a', {
  get: function() {
    console.log('get a');
    return 42;
  }
});
console.log(a); // prints "get a" followed by "42"

Great! We can now see when the global variable “a” is accessed. There aren’t many languages where you can do that. Hooray for JavaScript.

We may want to define more globals this way, so lets refactor this to use a helper:

function defineGlobal(name, value) {
  Object.defineProperty(global, name, {
    get: function() {
      console.log(`get ${name}`);
      return value;
    },
    configurable: true
  });
}
defineGlobal('a', 42);
console.log(a); // prints "get a" followed by "42"

Step 2: Calling the function

Now let’s look at the following statement:

a()

This is, unsurprisingly, a function call. It first evaluates a, as indicated above, by fetching a from the current scope. Then it calls a as a function. Nothing special going on here.

But to make this work with our a, we’re going to need to make sure that a is defined as a function, and not the value 42. So let’s change our getter to return a function:

defineGlobal('a', function() {
  console.log('call a');
  return 42;
});
console.log(a());
// get a
// call a
// 42

To answer our original question, we’re going to need to create a whole bunch of functions. So let’s again refactor this into a helper:

function defineFunction(name, body) {
  defineGlobal(name, function() {
    console.log(`call ${name}`);
    return body();
  });
}
defineFunction('a', () => 42);
console.log(a());

Step 3: Member access

The expression x[y], in JavaScript, is a property lookup. It evaluates the expressions x and y, and then finds the property on the object x that has the name resulting from the expression y. Here’s a snippet that illustrates this:

defineGlobal('x', { myProp: 42 });
defineGlobal('y', 'myProp');
console.log(x[y]);
// get x
// get y
// 42

If you’re not very familiar with JavaScript, it’s important to note here that the property name used here is "myProp", and not "y". The property name is the result of evaluating y.

Again, it will be useful to know exactly when the property is accessed, so let’s use a getter instead:

defineGlobal('x', {
  get myProp() {
    console.log('get x.myProp');
    return 42;
  }
});
defineGlobal('y', 'myProp');
console.log(x[y]);
// get x
// get y
// get x.myProp
// 42

Here I’ve just used the ES6 getter syntax, rather than using defineProperty.

As before, we’re going to need to do this a few times, so let’s create a helper function:

function createObject(objectName, propertyName, propertyValue) {
  return {
    get [propertyName]() {
      console.log(`get ${objectName}.${propertyName}`);
      return propertyValue;
    },
    set [propertyName](v) {
      console.log(`set ${objectName}.${propertyName}`);
      propertyValue = v;
    }
  };
}
defineGlobal('x', createObject('x', 'myProp', 42));
defineGlobal('y', 'myProp');
console.log(x[y]);

Step 4: Assignment

The last piece of the puzzle is the assignment operator. Consider the following code:

x = y

The assignment operator, like the other operators so far, will evaluate the each operand, and then perform some operation on the results. In the above case, x is evaluated, and then y is evaluated, and then the result of y is assigned to the result of x.

But wait. What do you mean “the result” of x?

The model here that JavaScript uses internally, is that x actually evaluates to a reference. This is a type in JavaScript which you’ve probably never heard of. A reference value consists of two components:

  • A base value, that tells you what container the value is stored in
  • A name, that tells you which value in the container is being referred to

In this case, the expression x evaluates to a reference that has the following attributes:

  • A base value that is the global object
  • A name that is the string "x"

In other words, the reference value is something like the English description “the property x on the global object”. When you assign to x, you are assigning to “the property x on the global object”. When you delete x, you are deleting “the property x on the global object”.

The expression y also evaluates to a reference, but the assignment operator coerces that reference to the actual referenced value. The same thing is done in expressions such as x + y or x(y).

Here’s another example of an assignment:

x.y = z

In this case, the base value of the reference is the object x, and the name is y.  The assignment sets the value referred to as “the property ‘y’ of the object x”. Similarly, you can do delete x.y to delete “the property ‘y’ of the object x”.

In a more detailed consideration of the above example, x and z evaluate to references. Both x and z are then coerced to values (dereferenced, by fetching the property or variable), and then a third reference is created refers to the property y on the base object x.

But, what order does this occur in? To find out, let’s use our trusty helper functions:

defineGlobal('x', createObject('x', 'y'));
defineGlobal('z', 42);
x.y = z
// get x
// get y
// set x.y

This might come as a little bit of a surprise. The expression x is evaluated before the expression y, and then the assignment takes place. In some ways, one expects the opposite — one expects that the left hand side of an assignment is not considered until the right hand side.

This seems to be a general rule in JavaScript. Operands are evaluated from left to right, and then the operator is executed. Perhaps an exception to this rule-of-thumb, is that the short-circuiting operators such as && must necessarily execute part of the operation without all the operands fully evaluated.

Side note: in languages such as C++, the order of the left and right hand side of a most operators is not defined. The compiler can chose to evaluate them in whatever order it thinks is best, or even evaluate them simultaneously (e.g. if the CPU has multiple cores). JavaScript is different, in that the specification lays out a specific, unambiguous ordering.

We can follow this to its logical conclusion, and determine the order of execution of the whole of the original program in question:

defineFunction('a', () => createObject('a', 'b'));
defineFunction('b', () => 'b');
defineFunction('c', () => createObject('c', 'd'));
defineFunction('d', () => 'd');
defineFunction('e', () => createObject('e', 'f'));
defineFunction('f', () => 'f');

a()[b()] = c()[d()] = e()[f()];
// get a
// call a
// get b
// call b
// get c
// call c
// get d
// call d
// get e
// call e
// get f
// call f
// get e.f
// set c.d
// set a.b

Can we abuse it? (Advanced)

The reason I started looking into this at all, is that I was trying to discover a way to “see” references. They are objects that exist in the execution model, but are never shown explicitly to the user of the language, so do they really need to exist at all?

This is import to me, because I’m writing a JavaScript compiler, and need to know whether references are best left as just a description mechanism in the ECMAScript specification, or if they should be considered to be real entities with real allocated memory in the runtime.

So, can we design an example, that unequivocally proves that there must be a reference allocated in memory at some point?

Here’s my attempt:

let resolveZ;

defineGlobal('z', new Promise(resolve => resolveZ = resolve));

async function asyncAssignment() {
  x[y] = await z;
}

defineGlobal('x', createObject('x1', 'y1'));
defineGlobal('y', 'y1');
asyncAssignment();
console.log('...it should be waiting for the result of z at this point...');
const o = createObject('x2', 'y2');
defineGlobal('x', o);
defineGlobal('y', 'y2');
asyncAssignment();
// Let's switch out property 'y2' for a new one, to make sure it's not holding a
// pointer to the property itself, but is instead recalling it by name
delete o.y2;
Object.defineProperty(o, 'y2', {
  set: value => {
    console.log(`set x.y2 (redefined) to ${value}`);
  }
});
asyncAssignment();
// And lastly, let's delete x from the global scope
delete x;
console.log('...now we are going to resolve the promise for z...');
resolveZ(42);
// get x
// get y
// get z
// ...it should be waiting for the result of z at this point...
// get x
// get y
// get z
// get x
// get y
// get z
// ...now we are going to resolve the promise for z...
// set x1.y1 to 42
// set x.y2 (redefined) to 42
// set x.y2 (redefined) to 42

What I’ve done here is break up the x[y] = z assignment using the await operator. The await operator will suspend the statement (and the rest of the async function), allowing us to swap out various things in the environment to see if we can mess with the operation while it is suspended. What we’re trying to prove here, is that the reference itself must be preserved in memory, from the time that the operation is suspended, to the time that it is resumed (when z is resolved).

To make it even more apparent, I’ve executed the async function multiple times, trying different ways to “mess” with the pending operations.

Conclusions

This experiment has proven to me that references are “almost” tangible objects. We can see that they must exist in memory under some circumstances, and that they are not simple “pointer” values — they must refer to both the object and the property name.

This leads to some interesting results when it comes to the order of evaluation of various expressions. While this knowledge isn’t needed for everyday programming scenarios, it helps to have a deeper understanding of what’s going on so that we know where the limit lies.

 

 

 

 


  1. Known in ECMAScript as a Lexical Environment 

  2. There is an interesting recursion here, since the value global here is also a globally scoped binding, which means the global property on the global object points to itself. You can see this if you have a statement like console.log(global.global.a) 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.