Problem
Let me explain what is happening using this handy diagram:
var p = function(o){
// 1 2 3
// v vvvvvvvvvvv vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
o.promise = new Promise((resolve,reject) => o = {resolve,reject});
// ^ ^ ^^^^^^^^^^^^^^^
// 6 5 4
console.log(o)
return o;
}({});
console.log(p);
- The assignment to
o.promise
starts. The engine will take a reference to o
first, then evaluate what to assign to the promise
property.
- The assignment expression is the call to the
Promise
constructor with new
. The engine has to evaluate the entire thing and get an object before it assigns anything to the promise
property.
- The
Promise
constructor receives an executor function. That function is immediately invoked. The promise construction cannot finish until the executor finishes.
- Inconsequential for the whole picture but added for clarity - an object is created with two properties
resolve
and reject
.
- The object is assigned to
o
. Note that step 1. is the start of the assignment. The assignment has not finished. It's midway. The code reassigns o
before the assignment to o.promise
completes. However, this happens after the engine has taken a reference to the initial object assigned to o
. There are now two objects related to o
:
- The one which was assigned at the start when step 1. takes place.
- The one that is assigned to it now - after step 5.
- (compacting things for brevity): the executor function completed. The
Promise
constructor also completed. The full expression new Promise((resolve,reject) => o = {resolve,reject})
has been evaluated and produced a value. The assignment to the promise
property can now continue. Since it uses the reference from step 1. the code assigns to the promise value of an object that is no longer assigned to o
.
- (nor pictured) the initial object that was assigned to
o
at step 1. has no more references to it. It is not eligible for garbage collection and will disappear from the memory. (the initially assigned) o
is dead, long live (the newly assigned) o
.
The whole situation can be simplified and represented by this code:
//get two references to the same object
let o1 = {};
let o2 = o1;
o1.promise = (() => o1 = {resolve: "foo", reject: "bar"})();
console.log(o1);
console.log(o2);
Since the Promise
constructor just invokes the executor function, it's replaced with an IIFE that behaves the same in regards to when evaluation of its body takes place. o1
and o2
represent the two resulting objects from evaluating the assignment line. Since the IIFE reassigns o1
, the o2
variable is there to show us what happened to the initial object. It does get a promise
property added to it but (without another reference) it's then lost.
With this in mind, we can see that a similar thing would happen in the original code, if we have another reference to the object which is given as an argument for o
:
//have a separate reference for the object passed below
const foo = {};
var p = function(o){
o.promise = new Promise((resolve,reject) => o = {resolve,reject});
console.log("o is", o)
return o;
}(foo);
console.log("p is", p);
//stack console shows `"promise": {}` but it's a promise object
//check the browser console if you want to see it
console.log("foo is", foo);
console.log("foo.promise is a Promise:", foo.promise instanceof Promise);
Solution
connexo shows that you can use Object.assign()
in another answer (included here for reference):
var p = function(o){
o.promise = new Promise((resolve,reject) => o = Object.assign(o, { resolve, reject }));
console.log(o)
return o;
}({});
console.log(p);
//usage
p.promise
.then(result => console.log(`Completed with ${result}`));
p.resolve(42);
This works because o
is not reassigned but enhanced with more properties. This is also why the version with the comma operator works - it still doesn't reassign o
, just modifies it:
o.promise = new Promise((resolve,reject) => ( o.resolve = resolve
, o.reject = reject
));
Another alternative is to use destructuring assignment onto object properties (as opposed to variables):
o.promise = new Promise((resolve,reject) => [o.resolve, o.reject] = [resolve, reject]);
Or if you want it more concise:
o.promise = new Promise((...rs) => [o.resolve, o.reject] = rs);
var p = function(o){
o.promise = new Promise((...rs) => [o.resolve, o.reject] = rs);
console.log(o)
return o;
}({});
console.log(p);
//usage
p.promise
.then(result => console.log(`Completed with ${result}`));
p.resolve(42);