Mysterious arguments object assignments

This past week, I found what I thought was a bug in Firefox’s JavaScript implementation and filed it. A response from Brendan Eich indicated that the behavior in question was, in fact, compliant with the spec and had been implemented for some time. I ran a few more tests to try and figure out where I had gone wrong. Indeed, Firefox, Internet Explorer, and Safari all exhibited the same behavior while Chrome did not. Here’s what happened.

The code

The code in question is as follows:

function doAdd(num1, num2) {
    if(arguments.length == 1) {
        arguments[1] = 10;
    }
    alert(arguments[0] + num2);
}
doAdd(10);

What would you expect the bottom line to output? In Chrome, it outputs 20, because assigning to arguments[1] also assigns to num2. In the other browsers, it outputs NaN, because assigning to arguments[1] does not also assign to num2. What exactly is going on here?

The spec

My confusion stemmed from Section 10.6 Note 1 of ECMA-262, 5th Edition, which reads:

For non-strict mode functions the array index (defined in 15.4) named data
properties of an arguments object whose numeric name values are less than
the number of formal parameters of the corresponding function object initially
share their values with the corresponding argument bindings in the function’s
execution context. This means that changing the property changes the
corresponding value of the argument binding and vice-versa.

I’ve discussed the similar clause before, at least the 3rd edition one, when answering Baranovskiy’s JavaScript quiz. I thought I had understood that arguments was always bound to the named arguments of the function. Both Tom Schuster and Brendan Eich pointed out that earlier in Section 10.6, in the instructions for creating the arguments object, the following appears:

args the actual arguments passed to the [[Call]] internal method

  1. Let len be the number of elements in args.
  2. Let indx = len – 1.
  3. Repeat while indx >= 0,

So the arguments object is created based on the number of actual arguments passed into the function and not on the number of named parameters defined for the function. That means, as Tom pointed out, the setter that would be created for the numeric index of the arguments object only applies to the number of arguments that were actually passed in. In my example, therefore, arguments[1] becomes a straight property assignment to the arguments object rather than calling the special setter that would copy the value to the named argument.

More code

So even though my previous example wouldn’t work in all browsers, this one will:

function doAdd(num1, num2) {
    arguments[1] = 10;
    alert(arguments[0] + num2);
}
doAdd(10, 25);   //20

The last line of this example outputs 20 in all browsers. Since I’m now passing in two arguments to the function, that means the arguments object is being created with two slots and therefore the special setter works for both indices 0 and 1. Setting arguments[1] in this code actually does update num2 and overwrites the value that was passed in.

Conclusion

Specifications are difficult to understand and even more difficult to implement. This was just a subtle reminder that there are dark corners of JavaScript where dragons lie. It’s fun to poke the dragons from time to time and learn exactly what they’ll do. Sometimes they’ll burn you, but you’ll learn either way.

Understanding JavaScript Promises E-book Cover

Demystify JavaScript promises with the e-book that explains not just concepts, but also real-world uses of promises.

Download the Free E-book!

The community edition of Understanding JavaScript Promises is a free download that arrives in minutes.