Why are properties on child array/object being set to the values of their highest indexed siblings?

Viewed 36

I have an class-defined object that contains an array of another class-defined object. The child class-defined object has an array of objects which come from a global array of objects.

In the code snippet below, I would expect the following:

  • Item #0: Counter: 1
  • Item #0: Counter: 2
  • Item #0: Counter: 3
  • Item #1: Counter: 4
  • Item #1: Counter: 5
  • Item #1: Counter: 6
  • Item #2: Counter: 7
  • Item #2: Counter: 8
  • Item #2: Counter: 9

But instead, the snippet below produces the output:

  • Item #0: Counter: 7
  • Item #0: Counter: 8
  • Item #0: Counter: 9
  • Item #1: Counter: 7
  • Item #1: Counter: 8
  • Item #1: Counter: 9
  • Item #2: Counter: 7
  • Item #2: Counter: 8
  • Item #2: Counter: 9

Somehow the earlier iterations of ItemObj.myData are replaced with newer records.

Why is this happening and more importantly, how can I avoid it and get my expected results?

const NUMBER_OF_ITEMS = 3;
const ARRAY_OF_OBJ = [{"id": 1, "color": "red"},{"id": 2, "color": "green"},{"id": 3, "color": "blue"}];
let GLOBAL_COUNTER = 0;

class ItemManager{
    constructor(){
        this.Items = [];
    for(let i = 0; i < NUMBER_OF_ITEMS; i++){
      this.Items.push(new ItemObj(i));
    }
  }  
}

class ItemObj{
    constructor(identifier){
    this.id = identifier;
    this.myData = [];
    this.getValues();
  }
  
  getValues (){
    for(let i = 0; i < ARRAY_OF_OBJ.length; i++ ){
        let myObj = Object.assign(ARRAY_OF_OBJ[i]);
      GLOBAL_COUNTER++;
      myObj.counter = GLOBAL_COUNTER;
      this.myData.push(myObj);
    }
  }
}

// Instantiate and display

const IM = new ItemManager();

for(let Item of IM.Items){
  for(let data of Item.myData){
    $("#output").append(`<li style="color: ${data.color}">Item #${Item.id}: Counter: ${data.counter} </li>`);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div >
<ul id="output"></ul>
</div>

2 Answers

You start out with 3 objects in the ARRAY_OF_OBJ array, which are then referenced multiple times whenever getValues is called:

let myObj = Object.assign(ARRAY_OF_OBJ[i]);

But you still only have 3 objects in memory; the myObj simply points to one of those three objects.

Passing a single object to Object.assign results in the same object - it does not clone it:

const obj = {};
const obj2 = Object.assign(obj);
console.log(obj === obj2);

You need to clone the object you retrieve from the array, otherwise there will still only be 3 objects in memory by the end:

let myObj = { ...ARRAY_OF_OBJ[i] };

const NUMBER_OF_ITEMS = 3;
const ARRAY_OF_OBJ = [{
  "id": 1,
  "color": "red"
}, {
  "id": 2,
  "color": "green"
}, {
  "id": 3,
  "color": "blue"
}];
let GLOBAL_COUNTER = 0;

class ItemManager {
  constructor() {
    this.Items = [];
    for (let i = 0; i < NUMBER_OF_ITEMS; i++) {
      this.Items.push(new ItemObj(i));
    }
  }
}

class ItemObj {
  constructor(identifier) {
    this.id = identifier;
    this.myData = [];
    this.getValues();
  }

  getValues() {
    for (let i = 0; i < ARRAY_OF_OBJ.length; i++) {
      let myObj = { ...ARRAY_OF_OBJ[i]
      };
      GLOBAL_COUNTER++;
      myObj.counter = GLOBAL_COUNTER;
      this.myData.push(myObj);
    }
  }
}

// Instantiate and display

const IM = new ItemManager();

for (let Item of IM.Items) {
  for (let data of Item.myData) {
    $("#output").append(`<li style="color: ${data.color}">Item #${Item.id}: Counter: ${data.counter} </li>`);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
  <ul id="output"></ul>
</div>

Use

let myObj = Object.assign({}, ARRAY_OF_OBJ[i]);

instead of

let myObj = Object.assign(ARRAY_OF_OBJ[i]);

as Object.assign() will refer to same object unless you provide and empty object as first argument. Providing empty object as first argument create new object and copies the object given as the second argument to it.

But remember it is still a shallow copy; nested objects will still be stored as references.

<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
 <div >
    <ul id="output"></ul>
    </div>
<script>
    const NUMBER_OF_ITEMS = 3;
    const ARRAY_OF_OBJ = [{"id": 1, "color": "red"},{"id": 2, "color": "green"},{"id": 3, "color": "blue"}];
    let GLOBAL_COUNTER = 0;

    class ItemManager{
        constructor(){
            this.Items = [];
        for(let i = 0; i < NUMBER_OF_ITEMS; i++){
          this.Items.push(new ItemObj(i));
        }
      }  
    }

    class ItemObj{
        constructor(identifier){
        this.id = identifier;
        this.myData = [];
        this.getValues();
      }
      
      getValues (){
        for(let i = 0; i < ARRAY_OF_OBJ.length; i++ ){
            let myObj = Object.assign({}, ARRAY_OF_OBJ[i]);
          GLOBAL_COUNTER++;
          myObj.counter = GLOBAL_COUNTER;
          this.myData.push(myObj);
        }
      }
    }

    // Instantiate and display

    const IM = new ItemManager();

    for(let Item of IM.Items){
      for(let data of Item.myData){
        $("#output").append(`<li style="color: ${data.color}">Item #${Item.id}: Counter: ${data.counter} </li>`);
      }
    }
</script>
</body>
</html>

Related