Function Arguments

Another example of a TDZ violation can be seen with ES6 default parameter values (see the ES6 & Beyond title of this series):

  1. var b = 3;
  2. function foo( a = 42, b = a + b + 5 ) {
  3. // ..
  4. }

The b reference in the assignment would happen in the TDZ for the parameter b (not pull in the outer b reference), so it will throw an error. However, the a in the assignment is fine since by that time it’s past the TDZ for parameter a.

When using ES6’s default parameter values, the default value is applied to the parameter if you either omit an argument, or you pass an undefined value in its place:

  1. function foo( a = 42, b = a + 1 ) {
  2. console.log( a, b );
  3. }
  4. foo(); // 42 43
  5. foo( undefined ); // 42 43
  6. foo( 5 ); // 5 6
  7. foo( void 0, 7 ); // 42 7
  8. foo( null ); // null 1

Note: null is coerced to a 0 value in the a + 1 expression. See Chapter 4 for more info.

From the ES6 default parameter values perspective, there’s no difference between omitting an argument and passing an undefined value. However, there is a way to detect the difference in some cases:

  1. function foo( a = 42, b = a + 1 ) {
  2. console.log(
  3. arguments.length, a, b,
  4. arguments[0], arguments[1]
  5. );
  6. }
  7. foo(); // 0 42 43 undefined undefined
  8. foo( 10 ); // 1 10 11 10 undefined
  9. foo( 10, undefined ); // 2 10 11 10 undefined
  10. foo( 10, null ); // 2 10 null 10 null

Even though the default parameter values are applied to the a and b parameters, if no arguments were passed in those slots, the arguments array will not have entries.

Conversely, if you pass an undefined argument explicitly, an entry will exist in the arguments array for that argument, but it will be undefined and not (necessarily) the same as the default value that was applied to the named parameter for that same slot.

While ES6 default parameter values can create divergence between the arguments array slot and the corresponding named parameter variable, this same disjointedness can also occur in tricky ways in ES5:

  1. function foo(a) {
  2. a = 42;
  3. console.log( arguments[0] );
  4. }
  5. foo( 2 ); // 42 (linked)
  6. foo(); // undefined (not linked)

If you pass an argument, the arguments slot and the named parameter are linked to always have the same value. If you omit the argument, no such linkage occurs.

But in strict mode, the linkage doesn’t exist regardless:

  1. function foo(a) {
  2. "use strict";
  3. a = 42;
  4. console.log( arguments[0] );
  5. }
  6. foo( 2 ); // 2 (not linked)
  7. foo(); // undefined (not linked)

It’s almost certainly a bad idea to ever rely on any such linkage, and in fact the linkage itself is a leaky abstraction that’s exposing an underlying implementation detail of the engine, rather than a properly designed feature.

Use of the arguments array has been deprecated (especially in favor of ES6 ... rest parameters — see the ES6 & Beyond title of this series), but that doesn’t mean that it’s all bad.

Prior to ES6, arguments is the only way to get an array of all passed arguments to pass along to other functions, which turns out to be quite useful. You can also mix named parameters with the arguments array and be safe, as long as you follow one simple rule: never refer to a named parameter and its corresponding arguments slot at the same time. If you avoid that bad practice, you’ll never expose the leaky linkage behavior.

  1. function foo(a) {
  2. console.log( a + arguments[1] ); // safe!
  3. }
  4. foo( 10, 32 ); // 42