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.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 ofbar.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 forthis
(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 tofunc.call(thisValue, ...argList)
in JavaScript, wherefunc === bar.baz.foo
andthisValue === bar.baz
.I hope, this visualization is of some use:
But the expressions
bar.foo()
,(bar.foo)()
, and similar ones likebar.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)()
orconst 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 betweenconst 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:
Note that passing something as an argument or returning something from a function also counts as an assignment.
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 atbar.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 aReferenceError
.(bar.foo)()
is the same asbar.foo()
. From the hugethis
answer:The runtime semantics only have one step and a note:
Sure enough,
delete
andtypeof
need to be able to accept a Reference Record, so they’re also “special” in the same way.