2

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.

Redu
  • 25,060
  • 6
  • 56
  • 76
  • 3
    I cannot spot any destructuring here. – connexo Mar 30 '21 at 19:26
  • What do you think `o` being reassigned to a simple object does to the original `o` and it's properties? – Randy Casburn Mar 30 '21 at 19:27
  • @connexo - this: `{resolve, reject}` in the reassignment of `o` (that obliterates the `promise` property the OP is looking for. – Randy Casburn Mar 30 '21 at 19:28
  • (at the risk of sounding like an ass:) This is precisely why (useless) contrived examples are useless. Try to find a practical use case for that code that produces `p` and you won't be able to. Trying to understand something in this manner has caused your distaste for destructuring because you've confused yourself with nonsense code. – Randy Casburn Mar 30 '21 at 19:31
  • 3
    @RandyCasburn That has nothing to do with destructuring, that is just object literal shorthand syntax. Compare https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#new_notations_in_ecmascript_2015 – connexo Mar 30 '21 at 19:35
  • Uh...nothing to do with destructuring. Basically nothing to do with promises, either. The entire contrived example simplifies to `let o = {}; o.whatever = (() => o = {foo:1,bar:2})();` - a reassignment of the object *as* you're assigning a property. – VLAZ Mar 30 '21 at 19:39
  • As for the question itself, you are re-assigning `o` to a simple plain new object with 2 properties. Of course the previous `promise` property is then lost, because it lives on an object that is no longer referenced by `o`. Try `o.promise = new Promise((resolve,reject) => o = { ...o, resolve, reject });`. And no, that `...o` ain't destructuring either, it's object spread syntax. – connexo Mar 30 '21 at 19:39
  • [See here for an illustration of how your code behaves.](https://jsbin.com/zopenacito/edit?js,console) You just change *the original object* as you're assigning a property to it. But the assigning of a property creates a new object. Normally, the original is just sacrificed to the GC and disappears but you can see it if you take a second reference to it. – VLAZ Mar 30 '21 at 19:44
  • @connexo Oh ...i didn't know Object literals were iterable appearantly they are now. Cool. – Redu Mar 30 '21 at 19:45
  • 2
    @Redu they aren't iterable. Spread syntax in an object literal is *different* to spreading an array. – VLAZ Mar 30 '21 at 19:45
  • You can also use `o.promise = new Promise((resolve,reject) => o = Object.assign(o, { resolve, reject }));`. Note that `Object.assign()` triggers setters, whereas spread syntax doesn't. – connexo Mar 30 '21 at 19:50
  • Thank you guys guys i got it.. Well except for the spread operator in object literals doing what it does without it being iterator but that's another question. – Redu Mar 30 '21 at 19:50
  • The spread syntax (commonly referenced as *operator*) copies own enumerable properties from one object into a new object. – connexo Mar 30 '21 at 19:51
  • 2
    [Check the beginning here](https://stackoverflow.com/a/64612639/). I try to explain the difference. – VLAZ Mar 30 '21 at 19:51
  • 1
    [Also covered to an extent here](https://stackoverflow.com/questions/44934828/is-it-spread-syntax-or-the-spread-operator) – VLAZ Mar 30 '21 at 19:53

2 Answers2

5

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.

connexo
  • 53,704
  • 14
  • 91
  • 128
  • Also worth noting that the promise is a red herring. There is nothing asynchronous happening - the executor given to the promise constructor is just executed immediately. It basically equivalent to an IIFE. – VLAZ Mar 30 '21 at 20:03
  • @VLAZ Actually while i did know that nothing async was going on i just couldn't help myself from suspecting. – Redu Mar 30 '21 at 20:10
  • @VLAZ Chrome having treated `setTimeout(func, 0)` like `setTimeout(func, 1)` in Chrome versions prior to version 89 is a different story. – connexo Mar 30 '21 at 20:12
  • @connexo BTW your object spreading example doesn't work in this case since `o` is still not populated with the `promise` property. – Redu Mar 30 '21 at 20:39
  • 1
    @VLAZ I just verified that @Redu is correct with their last comment. Any idea why spreading results in `{ reject: f(), resolve: f() }` whereas Object.assign results in `{ promise, reject: f(), resolve: f() }`? – connexo Mar 30 '21 at 20:53
  • @connexo While `o = Object.assign(o,{resolve,reject})` works... – Redu Mar 30 '21 at 20:54
  • @connexo because it's creating a new object again. The `promise` property is still assigned to the old object which is thrown away. The property doesn't exist *at the time of spreading*. – VLAZ Mar 30 '21 at 21:08
  • @VLAZ The old object clearly exists as a closure, why would it be thrown away? Also why wouldn't the property exist at the time of spreading, which is only when the promise resolves? Would the same so happen if we did `Object.assign({}, o, {reject, resolve})`? – connexo Mar 30 '21 at 21:12
  • 1
    @connexo when you do `o = { }` it throws away the old object and creates a new one. Having `o = {...o}` doesn't really help because there are no properties on `o`. It's the same as `o = {}`, which creates a new object and cuts off the reference to the old one making it GC eligible. This happens *before* the `o.promise` assignment - there the `o` still references the old object (just before it becomes GC fodder). There is no `promise` property to spread into the newly created object because it's not assigned yet. So, it's still [just this again](https://jsbin.com/delimiloye/1/edit). – VLAZ Mar 30 '21 at 21:17
  • @connexo as for `Object.assign` - yes. If you supply `{}` as first parameter. It still creates a new object then. If you drop `{}` and have `o` as first parameter, you'd just assign to the same `o` as before. – VLAZ Mar 30 '21 at 21:18
  • I suppose if you want to avoid `Object.assign()` *and* still just do a single expression in the executor, you could actual destructuring with `o.promise = new Promise((resolve,reject) => [o.resolve, o.reject] = [resolve,reject]);`. A but fugly. I'd personally use `Object.assign()`, though. – VLAZ Mar 30 '21 at 21:21
  • *This happens before the o.promise assignment* I cannot understand that, as the modification of `o` only occurs when the promise resolves. If it weren't assigned to the `o.promise` property, how can it resolve in the first place? Is this a specialty of how assigning promises works? – connexo Mar 30 '21 at 21:25
  • 1
    "*when the promise resolves.*" the promise doesn't resolve. As I said, the whole promise constructor is a red herring. Nothing async happens you give some function to the constructor and it's just immediately executed. Only differences with an IIFE is 1. the return value 2. the function will be called with two specific parameters. Both of these are irrelevant to the actual execution: `new Promise(fn)` is going to act the same as `fn()` in respect to when the content of the function is resolved. It's immediate. Also, `resolve` is never *called*, the promise is pending. – VLAZ Mar 30 '21 at 21:28
  • @VLAZ Thanks alot for clarifying this. I've never worked with promises, which shows in my comments. Need to get my head wrapped around them some time. – connexo Mar 30 '21 at 21:30
  • @connexo @VLAZ's [just this again](https://jsbin.com/delimiloye/1/edit?html,js,console,output) example is very intersting. That's a very peculiar behaviour especially when compared to `Object.assign()` case. – Redu Mar 30 '21 at 21:32
  • 2
    @Redu not really peculiar. It's just a lot of operations on one line. You need to read them right to left as that's the order of resolution. [It's basically this](https://jsbin.com/mifobaxiwa/1/edit?js,console) but more confusingly written. – VLAZ Mar 30 '21 at 21:35
3

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);
VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • I ended up doing like `o.promise = new Promise((...rs) => [o.resolve, o.reject] = rs)` as suggested in your last snippet. – Redu Apr 01 '21 at 16:54
  • 1
    @Redu Amended the answer to use `rs`. Didn't like `args` when I wrote it but I'm bad at naming things, so couldn't figure a better one. – VLAZ Apr 01 '21 at 16:59