Search code examples
javascriptreferenceclonefactorydeep-copy

How to prevent references from a constructed object back into a blueprint like source object which got assigned to the former target structure?


This is an example from a Pokemon-like game. I am constructing an Object, and inside it i am trying to make a new Object "en" and "to", that is two different attacks. The problem is that when i try to edit something in either of the attack Objects ("en" and "two"), the change happens to every Pokemon with the same name. This doesn't happen with the "health", so i think that the this.en = new Object; is the problem.

This is the code for constructing the Pokemon

function Fakemon(_navn, _type, _attackPower, _src,
    _1Navn, _1Force, _1Antall_, _2Navn, _2Force, _2Antall) {
    this.navn = _navn;
    this.type = _type;
    this.attackPower = _attackPower;
    this.src = _src;

    this.en = new Object;
    this.en.navn = _1Navn;
    this.en.force = _1Force;
    this.en.antall = _1Antall_;

    this.to = new Object;
    this.to.navn = _2Navn;
    this.to.force = _2Force;
    this.to.antall = _2Antall;

    this.health = 1000;

    console.log(this.en);
    this.pushFakemon = function() {
        fakemonSamling.push(this);
    }
    this.pushFakemon();
}

const fakemon1 = new Fakemon("BatCat", "Flying", [10, 50], ["batFront.png", "batBack.png"], "Blood Suck", [25, 38, 60], 10, "Wing Slap", [10, 17, 25], 20);
const fakemon2 = new Fakemon("Muffin Time", "Normal", [15, 45], ["cupcakeFront.png", "cupcakeBack.png"], "Frosting cover", [10, 17, 25], 20, "Cake stomp", [40, 50, 60], 5);

This is the Code for putting three Pokemon's to each player

for (let i = 0; i < 3; i++) {
        var temp1 = new Object;
        player1.push(Object.assign(temp1, randomFakemon()));
        var temp2 = new Object;
        player2.push(Object.assign(temp2, randomFakemon()));
    }

Solution

  • Apparently the OP has/had problems with two of the design choices.

    player1.push(Object.assign(temp1, randomFakemon())); ... picks a random true Fakemon instance from the OP's fakemon collection.

    Object.assign treats the source like an ordinary object. It assigns just the source object's enumerable own properties to the target object. Thus, nested, non primitive properties still will hold references to their's sources. The target object also will not change its type. It will not all of a sudden be an instance of Fakemon.

    The just described problem can be solved with a reliable own implementation of a clone function or, if available, by a structured clone.

    With the cloning approach a constructor, though it create own types, but does neither extend/subclass nor implement prototypal methods, renders useless.

    The only (own) method from the OP's example code is dispensable anyway, thus the suggested solution is a code refactoring towards a fakemon factory function and the usage of clone functionality.

    function createFakemon(base, attack1, attack2) {
      const fakemon = Object.assign({
          health: 1000,
        }, base, {
          en: { ...attack1 },
          to: { ...attack2 },
        });
      fakemonCollection.push(fakemon);
    
      return fakemon;
    };
    const fakemonCollection = []; // Dansk: fakemon samling.
    
    function getRandomFakemon() {
      return fakemonCollection[
        Math.floor(Math.random() * fakemonCollection.length)
      ];
    }
    
    const cloneDataStructure = (
      ('function' === typeof structuredClone) && structuredClone ||
      (value => JSON.parse(JSON.stringify(value)))
    );
    
    const fakemon1 = createFakemon({
      navn: 'BatCat',
      type: 'Flying',
      attackPower: [10, 50],
      sources: ['batFront.png', 'batBack.png'],
    }, {
      navn: 'Blood Suck',
      force: [25, 38, 60],
      antall: 10,
    }, {
      navn: 'Wing Slap',
      force: [10, 17, 25],
      antall: 20,
    });
    const fakemon2 = createFakemon({
      navn: 'Muffin Time',
      type: 'Normal',
      attackPower: [15, 45],
      sources: ['cupcakeFront.png', 'cupcakeBack.png'],
    }, {
      navn: 'Frosting cover',
      force: [10, 17, 25],
      antall: 20,
    }, {
      navn: 'Cake stomp',
      force: [40, 50, 60],
      antall: 5,
    });
    
    // create a(ny) player's fakemon as true clone
    // of a randomly picked `fakemonCollection` item.
    const playerFakemon = cloneDataStructure(
      getRandomFakemon()
    );
    // change a single attack value.
    playerFakemon.en.navn = 'Foo Bar';
    /*
      Thus instead of ...
    
      player1.push(Object.assign(temp1, randomFakemon()));
    
      ... one now would do ...
    
      player1.push(cloneDataStructure(getRandomFakemon()));
    */
    console.log({
      // no mutation at any of the collection's fakemon items.  
      fakemonCollection,
      // single different value for `playerFakemon.en.navn`.
      playerFakemon,
    });
    .as-console-wrapper { min-height: 100%!important; top: 0; }