Object destructuring resolve and reject in a Promise

Viewed 299

Object destructuring is not something i ike the most and i often try to avoid using it. However in this particular case i am too curious to neglect what is happening.

Now we can do like;

var o = {},
    x = 1,
    y = 2;
o = {x,y};
console.log(o); // <- {x: 1, y: 2}

all is fine;

My case is a little more complicated though;

var p = function(o){
          o.promise = new Promise((resolve,reject) => o = {resolve,reject});
          console.log(o)  // -< {resolve: ƒ, reject: ƒ}
          return o;
        }({});
console.log(p);           // -< {resolve: ƒ, reject: ƒ}

where is the promise property of p..? So i do it the classical way like;

var q = function(o){
          o.promise = new Promise((resolve,reject) => ( o.resolve = resolve
                                                      , o.reject  = reject
                                                      ));
          console.log(o)  // <- {promise: Promise, resolve: ƒ, reject: ƒ}
          return o;
        }({});
console.log(q);           // <- {promise: Promise, resolve: ƒ, reject: ƒ}

I have a strange feeling like i am missing something very fundamental but i can not tell what.

2 Answers

There is no destructuring in any of the code you've shown.

You are re-assigning o to a simple plain new object with 2 properties:

o = {resolve,reject}

which is just the object literal shorthand syntax introduced with ES2015 and identical to

o = {resolve: resolve, reject: reject}

Of course the previous promise property is then lost, because it lives on an object that is no longer referenced by o (and will eventually be garbage collected if there's no other reference to it in memory).

Try

o.promise = new Promise((resolve,reject) => o = { ...o, resolve, reject });

And no, that ...o isn't destructuring either, it's object spread syntax.

You can also use

o.promise = new Promise((resolve,reject) => o = Object.assign(o, { resolve, reject }));

Note that Object.assign() triggers setters (if any), whereas spread syntax doesn't.

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);
  1. The assignment to o.promise starts. The engine will take a reference to o first, then evaluate what to assign to the promise property.
  2. 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.
  3. The Promise constructor receives an executor function. That function is immediately invoked. The promise construction cannot finish until the executor finishes.
  4. Inconsequential for the whole picture but added for clarity - an object is created with two properties resolve and reject.
  5. 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.
  6. (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.
  7. (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);

Related