How does `this` work in default parameters?

329 views Asked by At

So... ES6¹ (which happens to be standardized a few hours ago) brings default parameters for functions similar to those in PHP, Python etc. I can do stuff like:

function foo (bar = 'dum') {
    return bar;
}

foo(1); // 1
foo(); // 'dum'
foo(undefined); // 'dum'

MDN says that the default value for the parameter is evaluated at call time. Which means each time I call the function, the expression 'dum' is evaluated again (unless the implementation does some weird optimizations which we don't care about).

My question is, how does this play into this?

let x = {
  foo (bar = this.foo) {
    return bar;
  }
}

let y = {
  z: x.foo
}

x.foo() === y.z(); // what?

The babel transpiler currently evaluates² it as false, but I don't get it. If they are really evaluated at call time, what about this:

let x = 'x from global';

function bar (thing = x) {
  return thing;
}

function foo () {
  let x = 'x from foo';
  return bar();
}

bar() === foo(); // what?

The babel transpiler currently evaluates³ it as true, but I don't get it. Why does bar not take the x from foo when called inside foo?

1 - Yes I know it is ES2015.
2 - Example A
3 - Example B

2

There are 2 answers

1
Bergi On BEST ANSWER

My question is, how does this play into this? I don't get it. Are they are really evaluated at call time?

Yes, the parameter initializers are evaluated at call time. It's complicated, but the steps are basically as follows:

  1. A new execution context is established on the stack,
    with a new environment in the "closure scope" of the called function
  2. If necessary, it's thisBinding is initialised
  3. Declarations are instantiated:
    1. Mutable bindings for the parameter names are created
    2. If necessary, an arguments object is created an bound
    3. The bindings are iteratively initialised from the arguments list (including all destructurings etc)
      In the course of this, initialisers are evaluated
    4. If any closures were involved, a new environment is inserted
    5. Mutable bindings for the variables declared in the function body are created (if not already done by parameter names) and initialised with undefined
    6. Bindings for let and const variables in the function body are created
    7. The bindings for functions (from function declarations in the body) are initialised with instantiated functions
  4. Finally the body of the function is evaluated.

So parameter initialisers do have access to the this and the arguments of the call, to previously initialised other parameters, and everything that is in their "upper" lexical scope. They are not affected by the variables declared in the function body (though they are affected by all the other parameters, even if in their temporal dead zone).

what about this:

function bar (thing = x) {}
{
  let x = 'x from foo';
  return bar();
}

I don't get it. Why does bar not take the x from foo when called inside foo?

Because x is a local variable that bar does not have access to. We're so lucky that they are not dynamically scoped! The parameter initialisers are not evaluated at the call site, but inside the called function's scope. In this case, the x identifier is resolved to the global x variable.

1
Travis Kaufman On

When they say "evaluated at call time", I think they're referring to a call-by-name expression. Here's how babel outputs your third example:

'use strict';

var x = 'x from global';

function bar() {
  var thing = arguments[0] === undefined ? x : arguments[0];

  return thing;
}

function foo() {
  var x = 'x from foo';
  return bar();
}

bar() === foo(); // what?

Since var x is inherited within the lexical scope of bar from the global scope, that is the scope in which it is used.

Now, consider the following:

let i = 0;

function id() {
  return i++;
}

function bar (thing = id()) {
  return thing;
}

console.info(bar() === bar()); // false

Which transpiles to

"use strict";

var i = 0;

function id() {
  return i++;
}

function bar() {
  var thing = arguments[0] === undefined ? id() : arguments[0];

  return thing;
}

console.info(bar() === bar()); // false

Notice how here, id is called inside the function, rather than being cached and memoized at the time the function is defined, hence the call-by-name rather than call-by-value.

So the behavior is actually correct in your second example. There is no y.foo and since this is dynamically scoped in Javascript (i.e. it varies based on the receiver of a given function invocation), when y.z() looks for this.foo, it will look for it in y, so y.z() will return undefined, while x.foo() will just return the foo function itself.

If you do want to bind to the receiver you can bind foo to x when you assign it. Then it should work as expected.

Sorry if any of this is unclear; let me know in the comments and I'd be happy to clarify! :)