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
?
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:
this
keyword work, and when should it be used?this
context when passing around members(0, _parseKey2.default)(something)
With the general explanation done, now let’s address some specific concerns of your question:
The expression bar.foo = bar.foo
returns a value; that value is the function object at bar.foo
.
Specifically, it must be a value of a language type, so it cannot be a Reference Record.
The specification says “Let rval be ? GetValue(rref)”, followed by “Return rval”.
In simplified terms, GetValue either returns a value of a language type or throws a ReferenceError
.
(bar.foo)()
is the same as bar.foo()
.
From the huge this
answer:
This is explained in this 2ality article (archived). Particularly see how a ParenthesizedExpression is evaluated.
The runtime semantics only have one step and a note:
ParenthesizedExpression :
(
Expression)
- Return ? Evaluation of Expression. This may be of type Reference.
Note
This algorithm does not apply GetValue to Evaluation of Expression. The principal motivation for this is so that operators such as
delete
andtypeof
may be applied to parenthesized expressions.
Sure enough, delete
and typeof
need to be able to accept a Reference Record, so they’re also “special” in the same way.