javascriptthisdynamic-scope

When invoking an assignment of a method to itself, why is `this` undefined?


This code defines an object bar and calls the function foo in various ways:

"use strict";

function foo() {
  return this;
}

let bar = {
  foo,
  baz() {
    return this;
  }
};

console.log( foo() ); // undefined (or globalThis outside strict mode)

console.log( bar.foo() ); // bar
console.log( (bar.foo)() ); // bar

console.log( (bar.foo = bar.foo)() ); // undefined (or globalThis outside strict mode)

Can someone please help me understand why (bar.foo = bar.foo)() is undefined?


Solution

  • When you call a function, there’s a special thing that happens in JavaScript engines; it’s a behavior that can be found in the specification and it is what allows the this value to be automatically bound in a method call.

    ECMAScript defines the syntax bar.baz.foo() as a CallMemberExpression.

       bar.baz.foo();
    // ^^^^^^^^^^^^^  CallMemberExpression
    // ^^^^^^^^^^^    ├─ MemberExpression
    // ^^^^^^^        │  ├─ MemberExpression
    //         ^^^    │  └─ IdentifierName
    //            ^^  └─ Arguments
    

    The way that this is evaluated involves splitting the CallMemberExpression into its constituent MemberExpression and Arguments. Then, the MemberExpression is evaluated which splits it into its constituent MemberExpression and IdentifierName. The MemberExpressions are all recursively processed, and evaluated as a single value of a language type (i.e. one of the familiar JavaScript types, usually the Object type).

    In the end, a value of a so-called specification type is generated: namely a Reference Record. These Reference Records are key–value pairs with four properties, but the relevant ones are [[Base]] and [[ReferencedName]]. The [[Base]] property contains the value of bar.baz (the evaluated nested MemberExpression), and the [[ReferencedName]] is the string "foo" (the string value of the IdentifierName). This is what the function call proceeds with.

    The specification types are distinct from the language types. Values of specification types are never observable in the language itself, and they might not actually exist. Specification types only “exist” to help explain concepts in the specification, but an implementation is free to choose whatever representation is suitable, as long as its behavior is equivalent to the normative specification text.

    The last step of the function call evaluation says “Return ? EvaluateCall(func, ref, arguments, tailCall)”, where func is the function object (of the language type Object) bar.baz.foo and ref is the Reference Record { [[Base]]: bar.baz, [[ReferencedName]]: "foo" }. And the last step of EvaluateCall says: “Return ? Call(func, thisValue, argList)”. When the function call is finally initiated here, it receives the function object to be invoked (func), the value for this (thisValue) which comes directly from the [[Base]] property of the Reference Record (except in a few special cases), and the argList from the Arguments. This looks very close to func.call(thisValue, ...argList) in JavaScript, where func === bar.baz.foo and thisValue === bar.baz.

    I hope, this visualization is of some use:

       bar.baz.foo();
    // ^^^^^^^^^^^^^  CallMemberExpression
    // ^^^^^^^^^^^    ├─ MemberExpression ────────────────────┐
    // ^^^^^^^        │  ├─ MemberExpression ─┐           (as object)
    //         ^^^    │  └─ IdentifierName ─┐ │               │                 ┌─────────────────────┐
    //            ^^  └─ Arguments ─┐       │ │ EvaluateCall( func, ref,        arguments, tailCall ) │
    //                              │       │ │               │     │           └───┐                 │
    //                              │       │ │         Call( func, │    thisValue, argList )         │
    //                              │       │ │     ┌───────────────┘    │                            │
    //                              │       │ │     Reference Record { [[Base]], [[ReferencedName]] } │
    //                              │       │ │                          │         │                  │
    //                              │       │ │                   (as object)  (as string)            │
    //                              │       │ └──────────────────────────┘         │                  │
    //                              │       └──────────────────────────────────────┘                  │
    //                              └─────────────────────────────────────────────────────────────────┘
    

    But the expressions bar.foo(), (bar.foo)(), and similar ones like bar.baz.foo(), (((bar.foo)))(), etc. are special because they uniquely keep the Reference Record for the function call. Almost all other expressions such as (bar.foo = bar.foo)(), (0, bar.foo)(), (null ?? bar.foo)(), etc. do not. This comes mostly from the fact that they’re simply evaluated differently; in other words: JavaScript just works this way because the spec says so.

    While theoretically possible to rewrite the spec and redesign the language such that (0, bar.foo)() or const foo = bar.foo; would keep the Reference Record or something similar (see Python with its bound methods), this would come with a huge compatibility impact, so we can’t really change the behavior. I think this behavior was chosen because JavaScript was originally designed to be a simple, easy to understand language, and the contextual distinction between const foo = (0, bar.foo); producing a value of a language type, but (0, bar.foo)() keeping a value of a specification type, was too complicated for the early purpose of JavaScript as a language for the Web.

    And even in the case of variable assignment, you lose the Reference Record, because you will be able to observe the assigned value, so it has to be of a language type:

    const foo1 = bar.foo; // Value `bar.foo` is observable by logging `foo1`.
    
    console.log(foo1); // A function object.
                       // You will never see ReferenceRecord { [[Base]]: bar, [[ReferencedName]]: "foo" } here, because this doesn’t exist in the language.
    

    Note that passing something as an argument or returning something from a function also counts as an assignment.

    const backcaller = (callback) => {
        // `callback` is a function object, not a Reference Record.
        callback();
    
        return callback;
      };
    
    backcaller(bar.foo)   // A return value must be a language type, so this is a function object, not a Reference Record.
                       ()
    

    See also:


    With the general explanation done, now let’s address some specific concerns of your question: