0

SUMMARY: A write operation to a single value in a JS Object (JSON format) modifies two values. (probably a copy vs reference bug).

UPDATE: JSFiddle with basic version of bug: https://jsfiddle.net/J_withfeeling/vmhx95yL/

FULL QUESTION:

I want to prep some data client side before writing it to a server.

I create my object like so:

let number = {};
let category = {};
number = {
   "numbers":{
      "num1":0,
      "num2":0,
      "num3":0
   }
};
console.log(categories);//confirming that categories is "{"category1":true,"category2":true}"
for(let m in majorList){//initialize the JSON object
   category[m] = Object.assign({}, number);
}
data = {major};

I now have a nice JS object, constructed in a JSON format:

   {
   "category":{
      "category1":{
         "numbers":{
            "num1":0,
            "num2":0,
            "num3":0
         }
      },
      "category2":{
         "numbers":{
            "num1":0,
            "num2":0,
            "num3":0
         }
      }
   }

I'm able to console.log(data) properly at this point with no problems.

Then with some JS I want to update the "num" values. I do this right now:

//some stuff up here to figure out which "category" and "number" to increment

      console.log(cat);
      console.log(num);
      console.log(JSON.stringify(data));
      data['category'][cat]['numbers'][num] = data['category'][cat]['numbers'][num] + 1;
      console.log(JSON.stringify(data));
//the above 5 lines are executed multiple times in a loop

What I expect to print out of those console.log statements is something like this:

category1
num2
"myJSON":{
   "category":{
      "category1":{
         "numbers":{
            "num1":0,
            "num2":0,
            "num3":0
         }
      },
      "category2":{
         "numbers":{
            "num1":0,
            "num2":0,
            "num3":0
         }
      }
   }
}
"myJSON":{
   "category":{
      "category1":{
         "numbers":{
            "num1":0,
            "num2":1,
            "num3":0
         }
      },
      "category2":{
         "numbers":{
            "num1":0,
            "num2":0,
            "num3":0
         }
      }
   }
}

What actually prints out is this:

category1
num2
"myJSON":{
   "category":{
      "category1":{
         "numbers":{
            "num1":0,
            "num2":0,
            "num3":0
         }
      },
      "category2":{
         "numbers":{
            "num1":0,
            "num2":0,
            "num3":0
         }
      }
   }
}
"myJSON":{
   "category":{
      "category1":{
         "numbers":{
            "num1":0,
            "num2":1,
            "num3":0
         }
      },
      "category2":{
         "numbers":{
            "num1":0,
            "num2":1,
            "num3":0
         }
      }
   }
}

The "num2" value is getting incremented in both of the "category" keys, in just a single pass of the innermost for loop. Why?

Admittingly above this code snippet there is a little more going on, but it's a bit much to include in a stackoverflow question. What is definitely the case is those 4 console.logs() with the one line of code in between. Those 5 lines are copied as-is, and I don't understand how one write operation can edit multiple values in the JSON object.

Jonathan
  • 73
  • 1
  • 2
  • 11
  • "category2:true ... you are missing " – MS90 Mar 31 '19 at 23:25
  • "I have a nice JSON object:" --- It's a JS object though. – zerkms Mar 31 '19 at 23:26
  • @MS90 my bad, thanks for the catch. That bug is not present in the original code. I've edited the question to try to simplify. – Jonathan Mar 31 '19 at 23:30
  • Sounds like you have same object references inside each category. How is this constructed? – charlietfl Mar 31 '19 at 23:30
  • @zerkms I guess I've always been a bit confused on this. Is it technically a JS Object that happens to have the same format as a JSON Object? – Jonathan Mar 31 '19 at 23:32
  • @Jonathan yep, it's a JS object literal, that is formatted to look like a JSON object. – zerkms Mar 31 '19 at 23:32
  • And please provide a runnable [mcve] that reproduces problem – charlietfl Mar 31 '19 at 23:33
  • @charlietfl I'm a bit confused by your comment. What do you mean by 'same object references'? As for how the Object is constructed, I'll update the question with some more details on that now... – Jonathan Mar 31 '19 at 23:34
  • I mean if you have a small object like `let nums={num1:0,num2:0}` and assign that object to 2 categories like `myJSON.cat1.numbers=nums` and `myJSON.cat2.numbers=nums` they share same object reference back to original `nums` object. Any change will be reflected to all instances because they are all the same *reference* – charlietfl Mar 31 '19 at 23:37
  • Change `category[m] = {score};` to `category[m] = Object.assign({}, score);` so you make a copy each time – charlietfl Mar 31 '19 at 23:44
  • 2
    Possible duplicate of [Is JavaScript a pass-by-reference or pass-by-value language?](https://stackoverflow.com/questions/518000/is-javascript-a-pass-by-reference-or-pass-by-value-language) – jhpratt Mar 31 '19 at 23:44
  • @charlietfl Thanks for the clarification. I do execute those 5 lines multiple times in a loop, and at some point both 'category' values are updated. You may be right that I'm getting a call-by-reference/call-by-value mixed up. I'm going to go explore that now; will update the question shortly. – Jonathan Mar 31 '19 at 23:45
  • I made the changes suggested by @ charlietfl but it did not make any difference in the values of the console logs. I'm reading more on Call By Sharing via the link @ jhpratt shared. – Jonathan Mar 31 '19 at 23:59
  • Just noticed nested score prop ... the spread approach below would work as would changing to `category[m].score = Object.assign({}, score.score)`. Bad design to start with – charlietfl Apr 01 '19 at 00:02
  • @charlietfl I added the nested "score" property to keep the same Object structure, without it it had one less layer (the "score" layer was mission). I can't do ```category[m].score``` because this is the first declaration of the object, [m].score is undefined. Thank you for helping me realize my reference/copy mixup, I'm going to keep searching if there is somewhere else I'm making the same mistake that may be causing the error to persist. – Jonathan Apr 01 '19 at 00:27
  • Would help to make a runnable demo [mcve] so we can inspect this issue in browser dev tools. Can set one up in any js sandbox site – charlietfl Apr 01 '19 at 00:29
  • Also I realized I wasn't being consistent when I changed some of the variable names from the source code to this question (I was alternating between 'score' and 'number' and such). Apoligies, that has now been fixed. @charlietfl I'll take that suggestion; I'll edit the question with a link to a JSFiddle shortly... – Jonathan Apr 01 '19 at 00:32

2 Answers2

0

When you do category[m] = {score}; you are creating a new object with a single property, which references the same object score every time. So when you update one of the score values, you update them all since they all refer to the same object.

You could do category = { score: {...score} } instead, that will create a new object.

leftclickben
  • 4,564
  • 23
  • 24
  • Thank you for the response, however it did not solve my issue. Making the modification you suggest, as well as a similar edit proposed by @charlietfl in the commets on the question above, still yeild the same bug where a single write operation modifies the values at two places in the object. – Jonathan Apr 01 '19 at 00:27
  • 1
    It sounds like you are changing deeply nested properties. The spread `...` operator will only shallow-copy the properties -- nested properties inside other objects will be copied by reference. In this case, the simplest solution is to deep clone the objects, and one way of doing this would be `category = { score: JSON.parse(JSON.stringify(score)) }`. Note this will fail in case of circular references; there is also a `lodash.deepClone()` function. – leftclickben Oct 14 '19 at 06:43
0

So, with @charlietfl pointing the way, I came up with a fix (if inelegant).

What is happening is indeed as @charlietfl and @leftclickben suggest, the JS Object is getting created using pass-by-reference, so multiple parts of the object have the same reference. Thus when one value gets updated, all the other values with the same reference also get updated.

To fix this, I moved these two lines of code:

let number = {};
number = {
   "numbers":{
      "num1":0,
      "num2":0,
      "num3":0
   }
};

from just above to inside of the loop, right before

category[m] = Object.assign({}, number);

This way, the 'number' object is destroyed and recreated between loop iterations, and so we can be sure that the next time it is used to build up the 'categories' object it is definitely a different reference. This eliminates the double-write problem. I've also updated the JSFiddle in the question with the working code snippet, plus some comments to serve as an abbreviated version of this explanation.

Shout out again to @charlietfl, if you want to post a more detailed answer I'd gladly endorse that.

Related posts: Is JavaScript a pass-by-reference or pass-by-value language? (as mentioned by @jhpratt). Also found this helpful: JavaScript: How to pass object by value?

Jonathan
  • 73
  • 1
  • 2
  • 11