@raina77ow recently helped me figure out computed property names. As part of their answer to my question, they shared a really tricky bit of code showcasing interesting aspects of JavaScript:
const increment = (() => { let x = 0; return () => ++x })();
const movingTarget = { toString: increment };
const weirdObjectLiteral = { [movingTarget]: 42 };
console.log( weirdObjectLiteral[movingTarget] ); // undefined
When I run that sample in the node CLI, that last line continually outputs undefined
, while the value x
in increment
continually increments.
If we replace const movingTarget = { toString: increment };
with const movingTarget = { [toString]: increment };
, this behaviour ceases to take place, and instead we get an output of 42
and the x
in increment
remains the same.
Can somebody help me understand why this is the case? What is it about JavaScript that makes things work this way?
Related Question: Does the x
in the function within increment
exist until we explicitly remove increment
from memory?
Lets evaluate the following object literal:
{[toString]: increment }
toString
is an identifier pointing to window.toString
(a function) As outlined by the answer, toString
will be called on that as object keys are always strings:
{[toString.toString()]: increment }
Now that results in something like:
{["function() { [native code] }"]: increment }
Now if we call toString()
on this object, the standard Object.prototype.toString
will get called in the {[movingTarget]: 42}
part and the result is [Object object]
(as always):
let x = 0;
let movingTarget = { ["function() { [native code] }"]: () => ++x };
console.log(
movingTarget.toString(), // [Object object]
{[movingTarget]: 42} // {["[Object object]"]: 42}
);
thats why the moving target isnt moving anymore. In the original code, toString
of the object was set, and that gets called whenever movingTarget
gets turned into a string:
let x = 0;
let movingTarget = { toString: () => ++x };
console.log(
movingTarget.toString(), // 1
"" + movingTarget, // 2
{[movingTarget]: 42} // { 3: 42 }
);
Let's dilute the complication a little bit by slightely changing the example. The following example is basically the same thing as toString
is called every time movingTarget
is evaluated, so we'll just get rid of it and call the function ourselves:
let x = 0;
let func = () => ++x;
const weirdObjectLiteral = { [func()]: 42 }; // equivalent to weirdObjectLiteral = { "1": 42 }
console.log( weirdObjectLiteral[func()] ); // equivalent to weirdObjectLiteral["2"]
See? The first time we called func
, the value it returned was 1
, so the "computed" property is "1"
. In the second time we called func
, the returned value was 2
, we tried accessing that and got undefined
back because there is no property "2"
.
How is this related to the example in the question?
It is related because in the original code we are using movingTarget
as both the value of the computed property and the key to access that property. Since both of them are expecting strings, movingTarget
is coerced into a string by calling its toString
method. This toString
method is defined as a function that increment x
and returns its new value (i.e. the inner function returned by the IIFE, the function () => ++x
). So basically whenever we used movingTarget
as either a computed property value or a key, that function is called and its return value was used.